Почему этот полиморфный код C # печатает то, что делает? - PullRequest
67 голосов
/ 02 октября 2009

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

// 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 (вот где я запутался!).


Вопрос:

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

Ответы [ 5 ]

96 голосов
/ 02 октября 2009

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

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

Давайте начнем с рассмотрения слотов. Есть два слота. Все экземпляры 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, и единственный слот, который вы можете найти, который соответствует, является GetNameSlotA. Вы не знаете о GetNameSlotC, потому что у вас есть только переменная типа A; почему вы ищете слоты, которые относятся только к C?

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

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

24 голосов
/ 02 октября 2009

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

3 голосов
/ 02 октября 2009

Легко, нужно только помнить дерево наследования.

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

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

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

2 голосов
/ 05 мая 2013

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

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

// 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. Возможно, мне следовало использовать термин метод вместо функции, но, как сказал Шекспир: функция с любым другим именем все еще является функцией:)

0 голосов
/ 02 октября 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...