¿Por qué 'ref' y 'out' no soportan el polimorfismo?
-
05-07-2019 - |
Pregunta
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é ocurre el error de compilación anterior? Esto sucede con los argumentos ref
y out
.
Solución
=============
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?
Vea la página del blog para más comentarios sobre este tema. Gracias por la gran pregunta.
=============
Supongamos que tienes clases Animal
, Mammal
, Reptile
, Giraffe
, Turtle y
Tiger
, con las relaciones obvias de subclasificación.
Ahora suponga que tiene un método void M (ref Mammal m)
. M
puede leer y escribir m
.
¿Puedes pasar una variable de tipo
Animal
aM
?
No. Esa variable podría contener un Turtle
, pero M
asumirá que solo contiene mamíferos. Un Turtle
no es un Mammal
.
Conclusión 1 : los parámetros de ref
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
Giraffe
aM
?
No. M
puede escribir en m
, y M
puede escribir un Tiger
en m
. Ahora ha puesto un Tiger
en una variable que en realidad es de tipo Giraffe
.
Conclusión 2 : los parámetros de ref
no se pueden hacer " más pequeños " ;.
Ahora considere N (out Mammal n)
.
¿Se puede pasar una variable de tipo
Giraffe
aN
?
No. N
puede escribir en n
, y N
puede escribir un Tiger
.
Conclusión 3 : los parámetros de out
no se pueden hacer " más pequeños " ;.
¿Puedes pasar una variable de tipo
Animal
aN
?
Hmm.
Bueno, ¿por qué no? N
no puede leer desde n
, solo puede escribir en él, ¿verdad? Escribe un Tiger
en una variable de tipo Animal
y ya está todo listo, ¿verdad?
Mal. La regla no es que " N
solo puede escribir en n
" ;.
Las reglas son, brevemente:
1) N
tiene que escribir en n
antes de que N
devuelva normalmente. (Si N
se lanza, todas las apuestas están desactivadas.)
2) N
tiene que escribir algo en n
antes de leer algo de n
.
Eso permite esta secuencia de eventos:
- Declare un campo
x
de tipoAnimal
. - Pase
x
como un parámetro deout
aN
. -
N
escribe unTiger
enn
, que es un alias parax
. - En otro hilo, alguien escribe un
Turtle
enx
. -
N
intenta leer el contenido den
y descubre unTurtle
en lo que cree que es una variable de tipoMammal
.
Claramente queremos hacer eso ilegal.
Conclusión 4 : los parámetros de out
no se pueden hacer " más grandes " ;.
Conclusión final : Ni los parámetros ref
ni out
pueden variar sus tipos. Hacer lo contrario es romper la seguridad del tipo verificable.
Si le interesan estos temas de la teoría de tipos básicos, considere leer mi serie sobre cómo funcionan la covarianza y la contravarianza en C # 4.0 .
Otros consejos
Porque en ambos casos debe poder asignar un valor al parámetro ref / out.
Si intentas pasar b al método Foo2 como referencia, y en Foo2 intentas asignar a = new A (), esto no sería válido.
La misma razón por la que no puedes escribir:
B b = new A();
Estás luchando con el problema clásico de OOP de covarianza (y contravarianza), consulta wikipedia : por mucho que este hecho pueda 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 elementos son asignables, para por el mismo motivo) al mismo tiempo que respeta principio de Liskov . Por qué esto es así, se esboza en las respuestas existentes y se explora con mayor profundidad en estos artículos de la wiki y sus enlaces.
Los lenguajes OOP que parecen hacerlo mientras se mantienen tradicionalmente estáticamente seguros para tipos son " engaño " (insertando controles de tipo dinámico 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 el desconcierto de los practicantes (como lo hace C # aquí), o pasar a un enfoque de escritura dinámica (como el primer lenguaje OOP, Smalltalk, hizo), o pasar al modo inmutable (individual) datos de asignación), al igual que los lenguajes funcionales (bajo la inmutabilidad, puede admitir la covarianza y también evitar otros rompecabezas relacionados, como el hecho de que no puede tener un rectángulo de subclase Cuadrado en un mundo de datos mutables).
Considera:
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
Mientras que las otras respuestas han explicado sucintamente el razonamiento detrás de este comportamiento, creo que vale la pena mencionar que si realmente necesitas hacer algo de esta naturaleza, puedes lograr una funcionalidad similar haciendo de Foo2 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 {}
}
Porque dar a Foo2
una ref B
resultaría en un objeto con formato incorrecto porque Foo2
solo sabe cómo rellenar A
parte de B
.
¿No es el compilador el que le dice que desea que emita el objeto explícitamente para que pueda estar seguro de saber cuáles son sus intenciones?
Foo2(ref (A)b)
Tiene sentido desde una perspectiva de seguridad, pero lo hubiera preferido si el compilador diera una advertencia en lugar de un error, ya que hay usos legítimos de objetos polimoprásicos pasados ??por referencia. por ejemplo
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á?
Si usa ejemplos prácticos para sus tipos, lo verá:
SqlConnection connection = new SqlConnection();
Foo(ref connection);
Y ahora tiene su función que toma el ancestro ( es decir Objeto
):
void Foo2(ref Object connection) { }
¿Qué puede estar mal con eso?
void Foo2(ref Object connection)
{
connection = new Bitmap();
}
Acabas de asignar un Bitmap
a tu SqlConnection
.
Eso no es bueno.
Inténtalo de nuevo con los demás:
SqlConnection conn = new SqlConnection();
Foo2(ref conn);
void Foo2(ref DbConnection connection)
{
conn = new OracleConnection();
}
Usted rellenó un OracleConnection
sobre la parte superior de su SqlConnection
.
En mi caso, mi función aceptó un objeto y no pude enviar nada, así que simplemente lo hice
object bla = myVar;
Foo(ref bla);
Y eso funciona
Mi 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