¿Por qué se usa la palabra clave 'out' en dos contextos aparentemente dispares?

11

En C #, la outpalabra clave se puede usar de dos maneras diferentes.

  1. Como modificador de parámetros en el que se pasa un argumento por referencia

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
  2. Como modificador de parámetros de tipo para especificar la covarianza .

    // Covariant interface. 
    interface ICovariant<out R> { }
    
    // Extending covariant interface. 
    interface IExtCovariant<out R> : ICovariant<R> { }
    
    // Implementing covariant interface. 
    class Sample<R> : ICovariant<R> { }
    
    class Program
    {
        static void Test()
        {
            ICovariant<Object> iobj = new Sample<Object>();
            ICovariant<String> istr = new Sample<String>();
    
            // You can assign istr to iobj because 
            // the ICovariant interface is covariant.
            iobj = istr;
        }
    }

Mi pregunta es: ¿por qué?

Para un principiante, la conexión entre los dos no parece intuitiva . El uso con genéricos no parece tener nada que ver con pasar por referencia.

Primero aprendí lo que outestaba relacionado con pasar argumentos por referencia, y esto dificultó mi comprensión del uso de la definición de covarianza con genéricos.

¿Hay una conexión entre estos usos que me falta?

Rowan Freeman
fuente
55
La conexión es un poco más comprensible si observa el uso de covarianza y contravarianza en System.Func<in T, out TResult>delegado .
rwong
44
Además, la mayoría de los diseñadores de idiomas intentan minimizar la cantidad de palabras clave, y agregar una nueva palabra clave en algún idioma existente con una base de código grande es doloroso (posible conflicto con algún código existente que usa esa palabra como nombre)
Basile Starynkevitch

Respuestas:

20

Hay una conexión, sin embargo, es un poco flojo. En C #, las palabras clave ´in´ y ´out´ como su nombre sugieren significan entrada y salida. Esto es muy claro en el caso de los parámetros de salida, pero es menos claro lo que tiene que ver con los parámetros de la plantilla.

Echemos un vistazo al principio de sustitución de Liskov :

...

El principio de Liskov impone algunos requisitos estándar a las firmas que se han adoptado en los nuevos lenguajes de programación orientados a objetos (generalmente a nivel de clases en lugar de tipos; ver subtipo nominal vs. estructural para la distinción):

  • Contravarianza de los argumentos del método en el subtipo.
  • Covarianza de los tipos de retorno en el subtipo.

...

¿Ves cómo la contravarianza está asociada con la entrada y la covarianza está asociada con la salida? En C #, si outmarca una variable de plantilla para hacerla covariante, pero tenga en cuenta que solo puede hacer esto si el parámetro de tipo mencionado solo aparece como salida (tipo de retorno de función). Entonces lo siguiente no es válido:

interface I<out T>
{
  void func(T t); //Invalid variance: The type parameter 'T' must be
                  //contravariantly valid on 'I<T>.func(T)'.
                  //'T' is covariant.

}

De manera similar si marca un parámetro de tipo con in, eso significa que solo puede usarlo como entrada (parámetro de función). Entonces lo siguiente no es válido:

interface I<in T>
{
  T func(); //Invalid variance: The type parameter 'T' must
            //be covariantly valid on 'I<T>.func()'. 
            //'T' is contravariant.

}

Para resumir, la conexión con la outpalabra clave es que con los parámetros de función significa que es un parámetro de salida , y para los parámetros de tipo significa que el tipo solo se usa en el contexto de salida .

System.FuncTambién es un buen ejemplo de lo que Rwong mencionó en su comentario. En System.Functodos los parámetros de entrada ar se ablandan con in, y el parámetro de salida se ablanda con out. La razón es exactamente lo que describí.

Gábor Angyal
fuente
2
¡Buena respuesta! Me ahorró un poco ... espera ... escribiendo! Por cierto: la parte del LSP que citó se conocía mucho antes de Liskov. Son solo las reglas de subtipo estándar para los tipos de función. (Los tipos de parámetros son contravariantes, los tipos de retorno son covariantes). La novedad del enfoque de Liskov fue a) formular las reglas no en términos de co / contravarianza sino en términos de sustituibilidad conductual (como se define por las condiciones previas / posteriores) yb) la regla de la historia , que hace posible aplicar todo este razonamiento a tipos de datos mutables, que anteriormente no era posible.
Jörg W Mittag
10

@ Gábor ya ha explicado la conexión (contravarianza para todo lo que entra ", covarianza para todo lo que sale"), pero ¿por qué reutilizar las palabras clave?

Bueno, las palabras clave son muy caras. No puede usarlos como identificadores en sus programas. Pero hay pocas palabras en el idioma inglés. Entonces, a veces te encuentras con conflictos, y tienes que cambiar el nombre de tus variables, métodos, campos, propiedades, clases, interfaces o estructuras para evitar chocar con una palabra clave. Por ejemplo, si estás modelando una escuela, ¿cómo llamas a una clase? No se puede llamar una clase, porque classes una palabra clave!

Agregar una palabra clave a un idioma es aún más costoso. Básicamente, hace que todo el código que usa esta palabra clave como identificador sea ilegal, rompiendo la compatibilidad con versiones anteriores en todo el lugar.

Las palabras clave iny outya existían, por lo que podrían reutilizarse.

Ellos podrían añadió claves contextuales que sólo son palabras clave en el contexto de una lista de parámetros de tipo, pero ¿qué palabras clave se han elegido? covarianty contravariant? +y -(como hizo Scala, por ejemplo)? supery extendscomo lo hizo Java? ¿Podría recordar desde la parte superior de su cabeza qué parámetros son covariantes y contravariantes?

Con la solución actual, hay una buena mnemotecnia: los parámetros de tipo de salida obtienen la outpalabra clave, los parámetros de tipo de entrada obtienen la inpalabra clave. Tenga en cuenta la buena simetría con los parámetros del método: los parámetros de salida obtienen la outpalabra clave, los parámetros de entrada obtienen la inpalabra clave (bueno, en realidad, no hay ninguna palabra clave en absoluto, ya que la entrada es la predeterminada, pero se entiende la idea).

[Nota: si miras el historial de edición, verás que en realidad cambié los dos en mi oración introductoria. ¡Y hasta recibí un voto positivo durante ese tiempo! Esto solo muestra lo importante que es realmente la mnemotecnia.]

Jörg W Mittag
fuente
La forma de recordar la covarianza contra la contravarianza es considerar lo que sucede si una función en una interfaz toma un parámetro de un tipo de interfaz genérico. Si uno tiene un interface Accepter<in T> { void Accept(T it);};, un Accepter<Foo<T>>aceptará Tcomo un parámetro de entrada si lo Foo<T>acepta como un parámetro de salida, y viceversa. Por lo tanto, contra -variance. Por el contrario, interface ISupplier<out T> { T get();};una Supplier<Foo<T>>tendrá cualquier tipo de variación Footiene - por lo tanto co -variance.
supercat