public class A
{
public virtual void Write() { Console.Write("A"); }
}
public class B : A
{
public override void Write() { Console.Write("B"); }
}
public class C : B
{
new public virtual void Write() { Console.Write("C"); }
}
public class D : C
{
public override void Write() { Console.Write("D"); }
}
// ...
D d = new D();
C c = d;
B b = c;
A a = b;
d.Write();
c.Write();
b.Write();
a.Write();
Почему на выходе получается DDBB, а не DDDD?
Почему D Write() не переопределяет B Write(), если фактический объект, на который ссылается b, — это D?





Причина в том, что в C у вас есть:
new public virtual void Write() { ... }
Использование new ... virtual здесь означает, что он больше не переопределяет B.Write() (который переопределяет A.Write()). Дополнительную информацию о модификаторе new смотрите в документации .
Затем D.Write() переопределяет метод Write в своем прямом базовом классе — C.
Поэтому, когда вы вызываете b.Write(); и a.Write(), этот метод Write не переопределяется в C или D, поэтому вызывается метод из B.
Если вы измените метод в C на использование override вместо new ... virtual:
public override void Write() { ... }
Все классы в вашей хирачи переопределят один и тот же метод в A, и вы получите ожидаемый результат (DDDD).
Чтобы по-настоящему понять, что происходит, нам нужна мысленная модель того, как поддерживается список методов типа (его таблица методов), чтобы обеспечить возможность виртуальных вызовов во время выполнения. В этом порядке он граничит с тремя регионами.
Если мы возьмем ваш пример класс за классом:
public class A {
public virtual void Write() { Console.Write("A"); }
}
virtual сам по себе создает новый виртуальный слот для метода в регионе 2. Он уже унаследовал 4 метода от System.Object, поэтому таблица методов A выглядит следующим образом:
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
--- end of region 1
--- start of region 2
5. Write (A::Write)
--- end of region 2
--- start of region 3
--- end of region 3
Далее у нас есть override для B
public class B : A {
public override void Write() { Console.Write("B"); }
}
Это повторно использует слот, который мы только что объявили в A, поэтому таблица B выглядит следующим образом (Write теперь находится в регионе 1, унаследованные виртуальные методы)
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
5. Write (still A::Write)
--- end of region 1
--- start of region 2
--- end of region 2
--- start of region 3
--- end of region 3
Тогда в C у нас есть new virtual
public class C : B {
public new virtual void Write() { Console.Write("C"); }
}
Это позволяет нам объявить новый виртуальный слот для этой реализации, опять же в регионе 2:
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
5. Write (A::Write)
--- end of region 1
--- start of region 2
6. Write (C::Write)
--- end of region 2
--- start of region 3
--- end of region 3
Наконец, если бы мы немного изменили ваш пример на D, чтобы не переопределять, а просто использовать new
public class D : C {
public new void Write() { Console.Write("D"); }
}
Это объявляет новый слот в регионе 3.
1. Object.ToString
2. Object.Equals
3. Object.GetHashCode
4. Finalize
5. Write (A::Write)
6. Write (C::Write)
--- end of region 1
--- start of region 2
--- end of region 2
--- start of region 3
7. Write (D::Write)
--- end of region 3
Компилятор C# устраняет неоднозначность, какой метод Write вызывать, фактически выдавая разные IL-коды в зависимости от полученного ссылочного типа:
D::WriteC::WriteA::WriteОн идет снизу вверх по таблице методов для типа ссылки на переменную, вызывающую метод, ищет подходящее имя, а затем генерирует разные вызовы метода IL для разных слотов:
D -> слот №7 (D::Write)C -> слот №6 (C::Write)B -> слот №5 (A::Write)A -> слот №5 (A::Write)Таким образом, для модифицированного примера IL будет таким:
// D d = new D();
IL_0001: newobj instance void D::.ctor()
// C c = d;
// B b = c;
// A a = b;
// d.Write();
IL_000e: callvirt instance void D::Write()
// c.Write();
IL_0015: callvirt instance void C::Write()
// b.Write();
IL_001c: callvirt instance void A::Write()
// a.Write();
IL_0023: callvirt instance void A::Write()
и результат DCBB.