Будет ли CLR проверять всю цепочку наследования, чтобы определить, какой виртуальный метод нужно вызвать?
Цепочка наследования следующая:
class A
{
public virtual void Foo()
{
Console.WriteLine("A method");
}
}
class B:A
{
public override void Foo()
{
Console.WriteLine("B method");
}
}
class C:B
{
public new virtual void Foo()
{
Console.WriteLine("C method");
}
}
class D:C
{
public override void Foo()
{
Console.WriteLine("D method");
}
}
то
class Program
{
static void Main(string[] args)
{
A tan = new D();
tan.Foo();
Console.Read();
}
}
В результате вызывается метод foo() в классе B.
Но в reference:
При вызове виртуального метода тип времени выполнения объекта проверяется на переопределение элемента. Превосходящий член в большинстве вызывается класс, который может быть исходным, если нет производный класс переопределил член.
В моей логике CLR сначала обнаруживает, что Foo()
является виртуальным методом, он просматривает таблицу методов D
, тип среды выполнения, затем обнаруживает, что в этом наиболее производном классе есть переопределяющий член, он должен назовите его и никогда не понимаете, что существует цепочка new Foo()
в цепочке наследования.
Что не так с моей логикой?
Ответы
Ответ 1
При вызове виртуального метода тип выполнения объекта проверяется для переопределяющего элемента. Вызывается переопределяющий член в самом производном классе, который может быть исходным, если ни один производный класс не переопределил член.
Вы начинаете с неправильного места. Ваша переменная имеет тип A
и содержит экземпляр D
, поэтому используемая виртуальная таблица A
1. Следуя приведенному выше тексту, мы проверяем переопределяющий элемент. Найдем один из B
. C
не учитывается, потому что он не является переопределяющим, он затеняет базовый метод. А поскольку D
переопределяет C
, а не A
или B
, это тоже не считается. Мы ищем переопределяющий элемент в самом производном классе.
Итак, найденный метод B.Foo()
.
Если вы измените C
, чтобы он переопределял вместо теней, найденный метод будет D
, потому что он является наиболее производным элементом переопределения.
Если вы вместо этого измените свой объект на экземпляр B
или C
, B.Foo()
по-прежнему будет выбранным переопределением. Чтобы уточнить, вот что я имею в виду:
A tan = new B();
tan.Foo(); // which foo is called? Why, the best Foo of course! B!
Причина B
вызывается потому, что цепочка наследования, которую мы ищем, охватывает от A
(тип переменной) до B
(тип времени выполнения). C
и D
больше не являются частью этой цепочки и не являются частью виртуальной таблицы.
Если вместо этого изменить код:
C tan = new D();
tan.Foo(); // which foo, which foo?
Цепочка наследования, которую мы ищем, охватывает от C
до D
. D
имеет переопределяющий элемент, поэтому он вызывается Foo
.
Предположим, вы добавили еще один класс Q
, который наследует от A
и R
, который наследуется от Q
и т.д. У вас есть две ветки наследования, не так ли? Что выбирается при поиске наиболее производного типа? Следуйте по пути от A
(ваш тип переменной) до D
(тип времени выполнения).
Надеюсь, это имеет смысл.
1 Не буквально. Виртуальная таблица относится к D
, поскольку она является типом среды выполнения и содержит все в цепочке наследования, но ее полезно и легче думать о A
как отправной точке. В конце концов, вы ищете производные типы.
Ответ 2
Ответ Эми правильный. Вот как мне нравится смотреть на этот вопрос.
Виртуальный метод - это слот, который может содержать метод.
При запросе разрешения перегрузки компилятор определяет, какой слот использовать во время компиляции. Но среда выполнения определяет, какой именно метод находится в этом слоте.
Теперь, имея в виду, рассмотрим ваш пример.
-
A
имеет один слот для Foo
.
-
B
имеет один слот для Foo
, унаследованный
от A
.
-
C
имеет два слота для Foo
. Один унаследован от B
и
один новый. Вы сказали, что хотите новый слот с именем Foo, так что вы его получили.
-
D
имеет два слота для Foo
, унаследованных от C
.
Это слоты. Итак, что происходит в этих слотах?
- В
A
, A.Foo
идет в слот.
- В
B
, B.Foo
идет в слот.
- В
C
, B.Foo
идет в первом слоте, а C.Foo
идет во второй слот. Помните, что эти слоты совершенно разные. Вы сказали, что хотите два слота с одинаковым именем, так что вы получили. Если это смущает, то ваша проблема. Не делайте этого, если это болит, когда вы это сделаете.
- В
D
, B.Foo
идет в первом слоте, а D.Foo
идет во второй слот.
Итак, что происходит с вашим звонком?
Компилятор объясняет, что вы вызываете Foo
на что-то типа времени компиляции A
, поэтому он находит первый (и только) слот Foo
на A
.
Во время выполнения содержимое этого слота B.Foo
.
Итак, что называется.
Теперь чувствуете смысл?