¿Por qué 'ref' y 'out' no admiten el polimorfismo?

124

Toma lo siguiente:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= compile-time error: 
                     // "The 'ref' argument doesn't match the parameter type"
    }

    void Foo(A a) {}

    void Foo2(ref A a) {}  
}

¿Por qué se produce el error en tiempo de compilación anterior? Esto sucede con ambos refy outargumentos.

Andreas Grech
fuente

Respuestas:

169

=============

ACTUALIZACIÓN: Utilicé esta respuesta como base para esta entrada de blog:

¿Por qué los parámetros ref y out no permiten la variación de tipo?

Consulte la página del blog para obtener más comentarios sobre este tema. Gracias por la gran pregunta.

=============

Supongamos que usted tiene clases Animal, Mammal, Reptile, Giraffe, Turtley Tiger, con las relaciones de subclases obvias.

Ahora suponga que tiene un método void M(ref Mammal m). MPuede leer y escribir tanto m.


¿Se puede pasar una variable de tipo Animala M?

No. Esa variable podría contener a Turtle, pero Msupondrá que solo contiene mamíferos. A Turtleno es a Mammal.

Conclusión 1 : los refparámetros no pueden hacerse "más grandes". (Hay más animales que mamíferos, por lo que la variable se está haciendo "más grande" porque puede contener más cosas).


¿Se puede pasar una variable de tipo Giraffea M?

No. Mpuede escribir my Mpuede querer escribir un correo Tigerelectrónico m. Ahora ha puesto una Tigervariable en una variable que en realidad es de tipo Giraffe.

Conclusión 2 : los refparámetros no pueden hacerse "más pequeños".


Ahora considera N(out Mammal n).

¿Se puede pasar una variable de tipo Giraffea N?

No. Npuede escribir ny Npuede querer escribir a Tiger.

Conclusión 3 : los outparámetros no pueden hacerse "más pequeños".


¿Se puede pasar una variable de tipo Animala N?

Hmm

¿Bueno, por qué no? Nno puede leer n, solo puede escribirle, ¿verdad? Escribes un Tigera una variable de tipo Animaly estás listo, ¿verdad?

Incorrecto. La regla no es " Nsolo se puede escribir n".

Las reglas son, brevemente:

1) Ntiene que escribir nantes de que Nvuelva normalmente. (Si Nlanza, todas las apuestas están canceladas).

2) Ntiene que escribir algo nantes de que lea algo n.

Eso permite esta secuencia de eventos:

  • Declarar un campo xde tipo Animal.
  • Pase xcomo outparámetro a N.
  • Nescribe un Tigeren n, que es un alias para x.
  • En otro hilo, alguien escribe un Turtleen x.
  • Nintenta leer el contenido de n, y descubre un Turtleen lo que piensa que es una variable de tipo Mammal.

Claramente queremos que sea ilegal.

Conclusión 4 : los outparámetros no pueden hacerse "más grandes".


Conclusión final : Ni reftampoco outparámetros pueden variar sus tipos. Hacer lo contrario es romper la seguridad de tipo verificable.

Si estas cuestiones en la teoría básica de tipos le interesan, considere leer mi serie sobre cómo funcionan la covarianza y la contravarianza en C # 4.0 .

Eric Lippert
fuente
66
+1. Gran explicación con ejemplos de clase del mundo real que demuestran claramente los problemas (es decir, explicar con A, B y C hace que sea más difícil demostrar por qué no funciona).
Grant Wagner el
44
Me siento humilde al leer este proceso de pensamiento. ¡Creo que mejor vuelvo a los libros!
Scott McKenzie
¡En este caso, realmente no podemos usar la variable de clase abstracta como argumentos y transmitir su objeto de clase derivado!
Prashant Cholachagudda
Aún así, ¿por qué los outparámetros no pueden hacerse "más grandes"? La secuencia que ha descrito se puede aplicar a cualquier variable, no solo a la outvariable de parámetro. Y también las necesidades de los lectores para emitir el valor del argumento a Mammalantes de intentar acceder a él como Mammaly por supuesto que puede fallar si no es considerado
ASTEF
29

Porque en ambos casos debe poder asignar un valor al parámetro ref / out.

Si intenta pasar b al método Foo2 como referencia, y en Foo2 intenta asignar a = new A (), esto sería inválido.
La misma razón por la que no puedes escribir:

B b = new A();
maciejkow
fuente
+1 Directo al grano y explica perfectamente bien el motivo.
Rui Craveiro
10

Estás luchando con el clásico problema OOP de covarianza (y contravarianza), mira wikipedia : aunque este hecho puede desafiar las expectativas intuitivas, es matemáticamente imposible permitir la sustitución de clases derivadas en lugar de las bases por argumentos mutables (asignables) (y también contenedores cuyos artículos son asignables, por la misma razón) sin dejar de respetar el principio de Liskov . Por qué eso es así se esboza en las respuestas existentes y se explora más profundamente en estos artículos wiki y enlaces a partir de ellas.

Los lenguajes OOP que parecen hacerlo mientras permanecen tradicionalmente estáticamente seguros de tipos son "trampas" (insertando verificaciones de tipos dinámicos ocultos o requiriendo un examen en tiempo de compilación de TODAS las fuentes para verificar); la elección fundamental es: renunciar a esta covarianza y aceptar la perplejidad de los profesionales (como lo hace C # aquí), o pasar a un enfoque de tipeo dinámico (como lo hizo el primer lenguaje OOP, Smalltalk), o pasar a inmutable (single- asignación) datos, como lo hacen los lenguajes funcionales (bajo inmutabilidad, puede admitir covarianza y también evitar otros acertijos relacionados, como el hecho de que no puede tener Rectángulo cuadrado de subclase en un mundo de datos mutables).

Alex Martelli
fuente
4

Considerar:

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

Violaría la seguridad de tipos

Henk Holterman
fuente
Es más el tipo inferido poco claro de "b" debido a la var que es el problema allí.
Supongo que en la línea 6 quisiste decir => B b = nulo;
Alejandro Miralles
@amiralles: sí, eso varestuvo totalmente mal. Fijo.
Henk Holterman
4

Si bien las otras respuestas han explicado sucintamente el razonamiento detrás de este comportamiento, creo que vale la pena mencionar que si realmente necesita hacer algo de esta naturaleza, puede lograr una funcionalidad similar al convertir Foo2 en un método genérico, como tal:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= no compile error!
    }

    void Foo(A a) {}

    void Foo2<AType> (ref AType a) where AType: A {}  
}
BrendanLoBuglio
fuente
2

Porque dar Foo2un ref Bresultado daría como resultado un objeto mal formado porque Foo2solo sabe cómo llenar Aparte de él B.

CaníbalSmith
fuente
0

¿No es ese el compilador que le dice que le gustaría que expulse el objeto explícitamente para que pueda estar seguro de cuáles son sus intenciones?

Foo2(ref (A)b)
dlamblin
fuente
No se puede hacer eso, "Un argumento ref o out debe ser una variable asignable"
0

Tiene sentido desde una perspectiva de seguridad, pero hubiera preferido que el compilador diera una advertencia en lugar de un error, ya que hay usos legítimos de objetos polimorfos pasados ​​por referencia. p.ej

class Derp : interfaceX
{
   int somevalue=0; //specified that this class contains somevalue by interfaceX
   public Derp(int val)
    {
    somevalue = val;
    }

}


void Foo(ref object obj){
    int result = (interfaceX)obj.somevalue;
    //do stuff to result variable... in my case data access
    obj = Activator.CreateInstance(obj.GetType(), result);
}

main()
{
   Derp x = new Derp();
   Foo(ref Derp);
}

Esto no se compilará, pero ¿funcionaría?

Oofpez
fuente
0

Si usa ejemplos prácticos para sus tipos, lo verá:

SqlConnection connection = new SqlConnection();
Foo(ref connection);

Y ahora tienes tu función que toma el ancestro ( es decir Object ):

void Foo2(ref Object connection) { }

¿Qué puede estar mal con eso?

void Foo2(ref Object connection)
{
   connection = new Bitmap();
}

Acabas de asignar una Bitmapa tu SqlConnection.

Eso no es bueno.


Intente nuevamente con otros:

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

void Foo2(ref DbConnection connection)
{
    conn = new OracleConnection();
}

Rellenaste OracleConnectiondemasiado SqlConnection.

Ian Boyd
fuente
0

En mi caso, mi función aceptó un objeto y no pude enviar nada, así que simplemente hice

object bla = myVar;
Foo(ref bla);

Y eso funciona

My Foo está en VB.NET y comprueba el tipo dentro y hace mucha lógica

Pido disculpas si mi respuesta es duplicada pero otras fueron demasiado largas

Shereef Marzouk
fuente