Путаница с виртуальным / новым / переопределением - PullRequest
8 голосов
/ 14 января 2010

Я немного озадачен вещью virtual / new / override. Вот пример:

class A
{
    public virtual void mVVirtual() { Console.WriteLine("A::mVVirtual"); }
}

class B : A
{
    public virtual void mVVirtual() { Console.WriteLine("B::mVVirtual"); }
}

class C : B
{
    public override void mVVirtual() { Console.WriteLine("C::mVVirtual"); }
}


class Test
{
    static void Main()
    {
        B b1 = new C();
        b1.mVVirtual();    //C::mVVirtual ... I understand this

        A a2 = new C();
        a2.mVVirtual();    //A::mVVirtual ... ???
    }
}

Я не понимаю, почему во втором звонке мы получаем A::mVVirtual. Я обычно отношусь к этим вопросам с помощью этого «алгоритма»:

  1. Проверьте тип переменной, содержащей ссылку для объекта, для метода экземпляра с именем mVVirtual? Не имеет ... но есть виртуальный метод с этой подписью и именем!
  2. Виртуальный метод? Затем давайте проверим тип объекта, удерживаемого a2 (C), для переопределения этого метода. У него есть один -> Выполнение C::mVVirtual!

Где мой "алгоритм" неверен? Я действительно смущен этим, и был бы очень признателен за некоторую помощь.

Ответы [ 5 ]

11 голосов
/ 14 января 2010

Вот как вы думаете о виртуальных методах. Каждый экземпляр класса имеет «ящики» для хранения методов. Когда вы помечаете метод как virtual, он говорит, что создайте новый «ящик» и поместите в него метод. Когда вы помечаете метод как override в производном классе, он удерживает «коробку» от базового класса, но помещает в него новый метод.

Итак, у вас есть класс A и метод с именем mVVirtual, помеченный как virtual. Это говорит: создайте новый «ящик» с именем mVVirtual и поместите в него метод с определением

Console.WriteLine("A::mVVirtual"); 

Затем у вас есть производный класс B и метод с именем mVVirtual, помеченный как virtual. Это говорит: создайте новый «ящик» с именем mVVirtual и поместите в него метод с определением

Console.WriteLine("B::mVVirtual"); 

В частности, «коробка», унаследованная от A, скрыта! Его нельзя увидеть объектами, которые напечатаны как B s, или классами, производными от B.

Затем у вас есть производный класс C и метод с именем mVVirtual, помеченный как override. Это говорит: возьмите «коробку» с именем mVVirtual, унаследованную от B, и поместите в нее другой метод с определением

Console.WriteLine("C::mVVirtual"); 

Теперь, когда у вас есть

B b1 = new C(); 
b1.mVVirtual();

вы говорите компилятору, что b1 - это B, так что b1.mVVirtual() смотрит в "коробку" mVVirtual и находит метод с определением

Console.WriteLine("C::mVVirtual"); 

потому что b1 на самом деле C, и это то, что находится в «коробке» mVVirtual для экземпляров C.

Но когда у вас есть

A a2 = new C(); 
a2.mVVirtual();

вы говорите компилятору, что a2 является A, и поэтому он выглядит в «коробке» и находит

Console.WriteLine("A::mVVirtual");

Компилятор не может знать, что a2 действительно является C (вы ввели его как A), поэтому он не знает, что a2 действительно является экземпляром класса, производного от класс, который скрыл «коробку» mVVirtual, определенную A. Он знает, что A имеет "ящик" с именем mVVirtual, и поэтому он генерирует код для вызова метода в этом "ящике".

Итак, чтобы попытаться выразить это кратко:

class A {
    public virtual void mVVirtual() { Console.WriteLine("A::mVVirtual"); }
}  

определяет класс, у которого есть «ящик» с полным именем A::mVVirtual, но к которому вы можете обращаться по имени mVVirtual.

class B : A 
{
    // "new" method; compiler will tell you that this should be marked "new" for clarity.
    public virtual void mVVirtual() { Console.WriteLine("B::mVVirtual"); }
}  

определяет класс, у которого есть «ящик» с полным именем B::mVVirtual, но к которому вы можете обращаться по имени mVVirtual. Ссылка на B.mVVirtual не будет ссылаться на «коробку» с полным именем A::mVVirtual; этот «ящик» не может быть виден объектами, которые напечатаны как B s (или классами, которые происходят от B).

class C : B
{
    public override void mVVirtual() { Console.WriteLine("C::mVVirtual"); }
}  

определяет класс, который принимает "коробку" с полным именем B::mVVirtual и помещает в нее другой метод.

Тогда

A a2 = new C(); 
a2.mVVirtual();

говорит, что a2 - это A, поэтому a2.mVVirtual ищет в «коробке» полное имя A::mVVirtual и вызывает метод в этом «ящике». Вот почему вы видите

A::mVVirtual

на консоли.

Есть еще два метода аннотирования. abstract делает новый «ящик», не помещает определение метода в «ящик». new создает новый «ящик» и помещает определение метода в «ящик», но не позволяет производным классам помещать свои собственные определения метода в «ящик» (используйте virtual, если хотите это сделать) .

Извините за скучность, но я надеюсь, что это поможет.

6 голосов
/ 14 января 2010

ОБНОВЛЕНИЕ: Для получения дополнительной информации об этой языковой функции см. Следующий вопрос здесь: Подробнее о Виртуальных / новых ... плюс интерфейсах!

Ответ Джейсона верен.Подводя итог более кратко.

У вас есть три метода.Назовите их MA, MB и MC.

У вас есть две «коробки» или, как их обычно называют, слоты.Мы будем придерживаться номенклатуры Джейсона.Назовите их BOX1 и BOX2.

«A» определяет BOX1.

«B» определяет BOX2.

«C» не определяет ни одного блока;он использует BOX2.

Когда вы говорите «new A ()», BOX1 заполняется MA.

Когда вы говорите «new B ()», BOX1 заполняется MA иBOX2 заполняется MB.

Когда вы говорите «new C ()», BOX1 заполняется MA, а BOX2 заполняется MC.

Теперь предположим, что у вас есть переменнаявведите A и вызов метода.Причина, как компилятор.Компилятор говорит "есть ли в типе A поля, соответствующие этому имени?"Да, есть один: BOX1.Поэтому компилятор генерирует вызов содержимого BOX1.

Как мы уже видели, содержимое BOX1 всегда является MA, поэтому MA всегда вызывается независимо от того, содержит ли переменная ссылку на A, B или C.

ТеперьПредположим, у вас есть переменная типа B и вызов метода.Опять же, думайте как компилятор.Компилятор говорит "есть ли в типе B поля, соответствующие этому имени?"Да, есть ДВА коробки, которые соответствуют по имени.Компилятор говорит, "какой из этих двух более тесно связан с B?"Ответ BOX2, потому что B объявляет BOX2.Поэтому компилятор генерирует вызов BOX2.

Это вызовет MB, если переменная содержит B, потому что в B BOX2 содержит MB.Это вызовет MC, если переменная содержит C, потому что в C BOX2 содержит MC.

Теперь понятно?Помните, разрешение перегрузки просто выбирает поле .Каково содержимое коробки, зависит от объекта во время выполнения.

3 голосов
/ 14 января 2010

У вас есть скрытые предупреждения? Когда я делаю то, что вы сделали, я получаю это предупреждение:

«ProjectName.ClassName.B.mVVirtual ()» скрывает унаследованный элемент «ProjectName.ClassName.A.mVVirtual ()». Чтобы текущий член переопределил эту реализацию, добавьте ключевое слово override. В противном случае добавьте новое ключевое слово.

Если бы вы использовали override в классе B, у вас не было бы этой проблемы; в обоих случаях вы получите "C :: mVVirtual". Поскольку вы не используете override в классе B, перед методом стоит неявное new. Это разрывает цепочку наследования. Ваш код вызывает метод типа A, и нет никаких наследующих классов, которые переопределяют этот метод из-за неявного new. Поэтому он должен вызывать реализацию класса А.

1 голос
/ 14 января 2010

Лучший способ понять, что виртуальные методы используют фактический (или конкретный ) тип объекта, который необходимо решить какую реализацию выполнить, когда не-виртуальные методы используют 'объявленный тип переменной, которую вы используете для доступа к методу, чтобы решить, какой запускать ...

Переопределить означает, что вы пишете метод, который собирается «заменить» реализацию виртуального или абстрактного метода (с тем же именем / сигнатурой) выше цепочки наследования.

new используется, когда в цепочке есть не виртуальный метод с тем же именем / сигнатурой, который метод, который вы добавляете, заменит ...

Разница в следующем

class base     { public virtual void  foo() { Console.write("base.foo"); } }
class derived  { public override void foo() { Console.write("derived.foo"); } }
base b = new base();
b.foo()  // prints "base.foo" // no issue b is a base and variable is a base
base b = new derived();
b.foo(); // prints "derived.foo" because concrete tyoe is derived, not base

но

class base     { public void  foo() { Console.write("base.foo"); } }
class derived  { public new void foo() { Console.write("derived.foo"); } }
base b = new base();
b.foo()  // prints "base.foo" // no issue b is a base and variable is a base
base b = new derived();
b.foo(); // prints "base.foo" because variable b is base. 
derived d = b as derived;
d.foo()    //  prints "derived.foo" - now variable d is declared as derived. 

Ваш второй вызов печатает A::mvVirtual, потому что этот метод (mVVirtual) на самом деле не виртуальный, (несмотря на его название), потому что он имеет новый спецификатор ... Таким образом, он решает, основываясь на типе переменной, который является A.

Чтобы объяснить, что происходит технически, у каждого типа есть «таблица методов» с указателями на все методы этого типа. (Это НЕ экземпляр типа, который имеет эту таблицу, это сам TYPE .) Таблица методов каждого Типа структурирована сначала всеми доступными виртуальными методами, начиная с object (дальше по цепочке наследования) в начале, до виртуальных методов, объявленных в самом типе, в конце. Затем, после представления всех виртуальных методов, все не виртуальные методы добавляются, опять же, из любых не виртуальных методов в object, во-первых, вплоть до любых не виртуальных методов в самом типе. Таблица структурирована таким образом, что смещения для всех виртуальных методов будут идентичны в таблицах методов всех производных классов, поскольку компилятор может вызывать эти методы из переменных, объявленных как другие типы, даже из кода в других методах, объявленных и реализованных в базе виды конкретного класса.

Когда компилятор разрешает вызов виртуального метода, он переходит в таблицу методов для самого типа объекта (тип конкретный ), тогда как для не виртуального вызова он идет в таблицу методов для объявленного типа переменной. Поэтому, если вы вызываете виртуальный метод, даже из кода базового типа, если конкретный конкретный тип имеет тип, производный от этого базового типа, компилятор переходит в таблицу методов для этого конкретного типа.

Если вы вызываете не виртуальный метод (независимо от того, насколько далеко может измениться наследование, может быть действительный тип объекта), компилятор обращается к таблице методов для объявленного типа переменной; Эта таблица содержит ничего от любых производных типов в дальнейшем по цепочке.

0 голосов
/ 14 января 2010

Вот как я понимаю

А это базовый класс
B наследует A, но не переопределяет его
C наследует B, но переопределяет его

Поскольку вы объявляете A, но инициализируете C, он будет игнорировать переопределение, поскольку базовый класс - это A, и A никогда не будет переопределено из B.

...