c# - virtuales - ¿Comprobará CLR toda la cadena de herencia para determinar a qué método virtual llamar?
virtual and override c# (2)
La cadena de herencia es la siguiente:
class A
{
public virtual void Foo()
{
Console.WriteLine("A''s method");
}
}
class B:A
{
public override void Foo()
{
Console.WriteLine("B''s method");
}
}
class C:B
{
public new virtual void Foo()
{
Console.WriteLine("C''s method");
}
}
class D:C
{
public override void Foo()
{
Console.WriteLine("D''s method");
}
}
entonces:
class Program
{
static void Main(string[] args)
{
A tan = new D();
tan.Foo();
Console.Read();
}
}
El resultado es que se llama al método foo () en la clase B.
Pero en la reference :
Cuando se invoca un método virtual, el tipo de tiempo de ejecución del objeto se comprueba para un miembro de reemplazo. Se llama al miembro que reemplaza en la clase más derivada, que podría ser el miembro original, si ninguna clase derivada ha reemplazado al miembro.
En mi lógica, CLR primero encuentra que Foo()
es un método virtual, busca en la tabla de métodos de D
, el tipo de tiempo de ejecución, luego descubre que hay un miembro sobresaliente en esta clase más derivada, debería llamarlo y nunca se da cuenta. Hay un new Foo()
en la cadena de herencia.
¿Qué hay de malo con mi lógica?
Cuando se invoca un método virtual, el tipo de tiempo de ejecución del objeto se comprueba para un miembro de reemplazo. Se llama al miembro que reemplaza en la clase más derivada, que podría ser el miembro original, si ninguna clase derivada ha reemplazado al miembro.
Estás empezando desde el lugar equivocado. Su variable es de tipo A
y contiene una instancia de D
, por lo que la tabla virtual utilizada es la de A
1 . Siguiendo el texto anterior, verificamos si hay un miembro de reemplazo. Encontramos uno en B
C
no cuenta porque no está anulando, está siguiendo el método base. Y como D
anula C
, no A
o B
, tampoco cuenta. Estamos buscando el miembro principal en la clase más derivada.
Así que el método encontrado es B.Foo()
.
Si cambia C
para que sobrescriba en lugar de las sombras, el método encontrado será D
, porque es el miembro sobrescrito más derivado.
Si, en cambio, cambia su objeto a una instancia de B
o C
, B.Foo()
seguirá siendo la anulación elegida. Para aclarar, esto es lo que quiero decir:
A tan = new B();
tan.Foo(); // which foo is called? Why, the best Foo of course! B!
La razón por la que se llama B
es porque la cadena de herencia que estamos buscando abarca desde A
(el tipo de variable) hasta B
(el tipo de tiempo de ejecución). C
y D
ya no forman parte de esa cadena y no forman parte de la tabla virtual.
Si en cambio cambiamos el código a esto:
C tan = new D();
tan.Foo(); // which foo, which foo?
La cadena de herencia que buscamos abarca desde C
hasta D
D
es un miembro primordial, por lo que se llama Foo
.
Supongamos que agrega otra clase Q
que hereda de A
, y R
que hereda de Q
, y así sucesivamente. Tienes dos ramas de la herencia, ¿verdad? ¿Cuál se elige al buscar el tipo más derivado? Siga la ruta de A
(su tipo de variable) a D
(el tipo de tiempo de ejecución).
Espero que esto tenga sentido.
1 No literalmente. La tabla virtual pertenece a D
porque es el tipo de tiempo de ejecución y contiene todo en su cadena de herencia, pero es útil y más fácil pensar en A
como el punto de partida. Estás buscando tipos derivados, después de todo.
La respuesta de Amy es correcta. Así es como me gusta mirar esta pregunta.
Un método virtual es una ranura que puede contener un método .
Cuando se le pide que realice una resolución de sobrecarga, el compilador determina qué ranura usar en el momento de la compilación . Pero el tiempo de ejecución determina qué método está realmente en esa ranura .
Ahora con eso en mente, veamos su ejemplo.
-
A
tiene una ranura paraFoo
. -
B
tiene una ranura paraFoo
, heredada deA
-
C
tiene dos ranuras paraFoo
. Una heredada deB
, y una nueva. Dijiste que querías un nuevo espacio llamado Foo , así que lo conseguiste. -
D
tiene dos ranuras paraFoo
, heredadas deC
Esos son los slots. Entonces, ¿qué pasa en esas ranuras?
- En una
A
,A.Foo
entra en la ranura. - En una
B
,B.Foo
va en la ranura. - En una
C
,B.Foo
va en la primera ranura yC.Foo
va en la segunda ranura. Recuerda, estas ranuras son completamente diferentes . Dijiste que querías dos ranuras con el mismo nombre, así que eso es lo que tienes. Si eso es confuso, ese es tu problema. No hagas eso si te duele cuando lo haces. - En una
D
,B.Foo
va en la primera ranura yD.Foo
va en la segunda ranura.
¿Y ahora qué pasa con tu llamada?
El compilador indica que estás llamando a Foo
en algo de tiempo de compilación tipo A
, por lo que encuentra la primera (y única) ranura de Foo
en A
En tiempo de ejecución, el contenido de esa ranura es B.Foo
.
Así que eso es lo que se llama.
¿Tiene sentido ahora?