Почему этот полиморфный код на С# печатает то, что он делает?

Недавно я получил следующий фрагмент кода как своего рода головоломку, чтобы помочь понять Polymorphism и Inheritance в OOP - С#.

// No compiling!
public class A
{
     public virtual string GetName()
     {
          return "A";
     }
 }

 public class B:A
 {
     public override string GetName()
     {
         return "B";
     }
 }

 public class C:B
 {
     public new string GetName()
     {
         return "C";
     }
 }

 void Main()
 {
     A instance = new C();
     Console.WriteLine(instance.GetName());
 }
 // No compiling!

Теперь, после долгой, продолжительной беседы с другим разработчиком, который представил загадку, я знаю, какой результат, но я не испорчу его для вас. Единственная проблема, с которой я действительно сталкиваюсь, - это то, как мы получаем этот вывод, как код проходит, что наследует что и т.д.

Я думал, что C будет возвращен, поскольку это, кажется, класс, который определен. Затем я просмотрел вопрос о том, будет ли возвращен B, потому что C наследует B - но B также наследует A (где я запутался!).


Вопрос:

Может ли кто-нибудь объяснить, как полиморфизм и наследование играют свою роль в извлечении вывода, в конечном счете отображаемом на экране?

Ответ 1

Правильный способ думать об этом состоит в том, чтобы представить, что каждый класс требует, чтобы его объекты имели определенное количество "слотов"; эти слоты заполнены методами. Вопрос "какой метод на самом деле вызван?" требует, чтобы вы выяснили две вещи:

  • Какое содержимое каждого слота?
  • Какой слот называется?

Давайте начнем с рассмотрения слотов. Есть два слота. Для всех экземпляров A требуется слот, который мы будем называть GetNameSlotA. Для всех экземпляров C требуется слот, который мы будем называть GetNameSlotC. То, что означает "новое" в декларации в C, означает "я хочу новый слот". По сравнению с "переопределением" в объявлении в B, что означает "я не хочу новый слот, я хочу повторно использовать GetNameSlotA".

Конечно, C наследует от A, поэтому C также должен иметь слот GetNameSlotA. Поэтому экземпляры C имеют два слота - GetNameSlotA и GetNameSlotC. Экземпляры A или B, которые не являются C, имеют один слот, GetNameSlotA.

Теперь, что происходит в этих двух слотах, когда вы создаете новый C? Существует три метода, которые мы будем называть GetNameA, GetNameB и GetNameC.

В декларации A говорится: "Вставьте GetNameA в GetNameSlotA". A является суперклассом C, поэтому правило A относится к C.

В декларации B говорится: "Поместите GetNameB в GetNameSlotA". B является суперклассом C, поэтому правило B применяется к экземплярам C. Теперь у нас есть конфликт между A и B. B - это более производный тип, поэтому он выигрывает - правило B переопределяет правило A. Следовательно, слово "переопределить" в декларации.

В декларации C говорится: "Поместите GetNameC в GetNameSlotC".

Поэтому ваш новый C будет иметь два слота. GetNameSlotA будет содержать GetNameB, а GetNameSlotC будет содержать GetNameC.

Теперь мы определили, какие методы находятся в слотах, поэтому мы ответили на наш первый вопрос.

Теперь мы должны ответить на второй вопрос. Какой слот называется?

Подумайте об этом, как о компиляторе. У вас есть переменная. Все, что вы знаете об этом, это то, что оно имеет тип A. Вам предлагается разрешить вызов метода для этой переменной. Вы смотрите на слоты, доступные на A, и единственный слот, который вы можете найти, - это GetNameSlotA. Вы не знаете о GetNameSlotC, потому что у вас есть только переменная типа A; почему вы ищете слоты, которые применяются только к C?

Следовательно, это вызов того, что находится в GetNameSlotA. Мы уже определили, что во время выполнения GetNameB будет в этом слоте. Следовательно, это вызов GetNameB.

Ключевым выводом здесь является то, что в разрешении перегрузки С# выбирает слот и генерирует вызов того, что происходит в этом слоте.

Ответ 2

Он должен возвращать "B", потому что B.GetName() удерживается в маленьком поле виртуальной таблицы для функции A.GetName(). C.GetName() - это время переопределения времени компиляции, оно не переопределяет виртуальную таблицу, поэтому вы не можете получить ее с помощью указателя на A.

Ответ 3

Просто, вам нужно сохранить дерево наследования.

В вашем коде вы держите ссылку на класс типа "A", который создается экземпляром типа "C". Теперь, чтобы разрешить точный адрес метода для виртуального метода GetName(), компилятор поднимает иерархию наследования и ищет последнее переопределение (обратите внимание, что только "виртуальный" является переопределением, "новый" - это нечто совершенно другое...).

Короче, что происходит. Новое ключевое слово из типа 'C' будет играть только роль, если вы вызовете его в экземпляре типа 'C', а компилятор затем полностью сменит все возможные отношения наследования. Строго говоря, это не имеет ничего общего с полиморфизмом - вы можете видеть, что из-за того, что вы маскируете виртуальный или не виртуальный метод с помощью "нового" ключевого слова, не имеет никакого значения...

"Новый" в классе "С" означает именно это: если вы вызываете "GetName()" в экземпляре этого (точного) типа, то забудьте все и используйте ЭТОТ метод. "Виртуальный", наоборот: перейдите в дерево наследования, пока не найдете метод с этим именем, независимо от того, какой именно тип вызывающего экземпляра.

Ответ 4

ОК, сообщение немного устарело, но это отличный вопрос и отличный ответ, поэтому я просто хотел добавить свои мысли.

Рассмотрим следующий пример, который аналогичен предыдущему, кроме основной функции:

// No compiling!
public class A
{
    public virtual string GetName()
    {
        return "A";
    }
}

public class B:A
{
    public override string GetName()
    {
        return "B";
    }
}

public class C:B
{
    public new string GetName()
    {
        return "C";
    }
}

void Main()
{
    Console.Write ( "Type a or c: " );
    string input = Console.ReadLine();

    A instance = null;
    if      ( input == "a" )   instance = new A();
    else if ( input == "c" )   instance = new C();

   Console.WriteLine( instance.GetName() );
}
// No compiling!

Теперь действительно очевидно, что вызов функции не может быть привязан к определенной функции во время компиляции. Однако необходимо компилировать что-то, и эта информация может зависеть только от типа ссылки. Таким образом, было бы невозможно выполнить функцию GetName класса C с любой ссылкой, отличной от типа C.

P.S. Возможно, я должен был использовать термин метод вместо функции, но, как сказал Шекспир: Функция любым другим именем все еще является функцией:)

Ответ 5

На самом деле, я думаю, он должен отображать C, потому что новый оператор просто скрывает методы all предков с тем же именем. Таким образом, при скрытых методах A и B остается только C.

http://msdn.microsoft.com/en-us/library/51y09td4%28VS.71%29.aspx#vclrfnew_newmodifier