Почему «новый» метод блокирует переопределение?

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?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
131
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Причина в том, что в 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).

Ответ принят как подходящий

Чтобы по-настоящему понять, что происходит, нам нужна мысленная модель того, как поддерживается список методов типа (его таблица методов), чтобы обеспечить возможность виртуальных вызовов во время выполнения. В этом порядке он граничит с тремя регионами.

  1. Унаследованные виртуальные методы
  2. Объявленные виртуальные методы
  3. Методы экземпляра

Если мы возьмем ваш пример класс за классом:

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-коды в зависимости от полученного ссылочного типа:

  1. D::Write
  2. C::Write
  3. A::Write

Он идет снизу вверх по таблице методов для типа ссылки на переменную, вызывающую метод, ищет подходящее имя, а затем генерирует разные вызовы метода IL для разных слотов:

  1. Для справки D -> слот №7 (D::Write)
  2. Для справки C -> слот №6 (C::Write)
  3. Для справки B -> слот №5 (A::Write)
  4. Для справки 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.

Другие вопросы по теме

Почему я не могу получить доступ к общедоступному методу после переопределения защищенного метода?
Определение конкретной функциональности класса с использованием декораторов в качестве информации о переопределениях классов
Как переопределить реализацию функций протокола UnkeyedDecodingContainer по умолчанию?
Переопределить blpapi при получении данных Bloomberg (Python)
Невозможно переопределить изменяемое свойство свойством, доступным только для чтения, которое является переменной
Какие методы, унаследованные от класса Object, обычно следует переопределять?
Почему мы не можем реализовать два интерфейса с методами с одинаковой сигнатурой, один из которых имеет реализацию по умолчанию в Java?
Как сделать так, чтобы метод подкласса, унаследованный от pathlib.Path, возвращал Path вместо подкласса
Переобъявить оператор «==" внутри типа расширения — Dart
Как переопределить переменные стиля antd при использовании scss?