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::Write
C::Write
A::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
.