Разрешение не виртуального метода - почему это происходит

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

Взгляните на код ниже.

class Program
{
    static void Main(string[] args)
    {
        Sedan vehicle = new Sedan();
        vehicle.Drive();
        vehicle.Accelerate();
    }
}

abstract class VehicleBase
{
    public void Drive()
    {
        ShiftIntoGear();
        Accelerate();
        Steer();
    }

    protected abstract void ShiftIntoGear();
    protected abstract void Steer();

    public void Accelerate()
    {
        Console.WriteLine("VehicleBase.Accelerate");
    }
}

class Sedan : VehicleBase
{
    protected override void ShiftIntoGear()
    {
        Console.WriteLine("Sedan.ShiftIntoGear");
    }

    protected override void Steer()
    {
        Console.WriteLine("Sedan.Steer");
    }

    public new void Accelerate()
    {
        Console.WriteLine("Sedan.Accelerate");
    }
}

Окна консоли показывают следующее:

Sedan.ShiftIntoGear
VehicleBase.Accelerate
Sedan.Steer
Sedan.Accelerate

Для меня это не имеет смысла, и я считаю, что многие люди запутаются. Если теперь вы объявите переменную транспортное средство типа VehicleBase, вы получите

Sedan.ShiftIntoGear
VehicleBase.Accelerate
Sedan.Steer
VehicleBase.Accelerate

Это то, что я ожидал и в предыдущем случае, потому что метод Accelerate не является виртуальным.

В предыдущем выводе (с переменным транспортным средством, типизированным как Sedan, я ожидаю, что Sedan.Accelerate будет вызываться вместо VehicleBase.Accelerate. В нынешнем виде, в зависимости от того, откуда вы его вызываете (из класса или извне) поведение меняется.

Мне кажется, что правила разрешения перегрузок для повторно введенных методов имеют приоритет, но мне трудно поверить, что это правильное/ожидаемое поведение.


person Shiv Kumar    schedule 15.12.2012    source источник
comment
Не уверен, что вы находите здесь недоумение. Можете ли вы объяснить, что вы считаете проблемой?   -  person Oded    schedule 16.12.2012
comment
Я думал, что сделал. Были предоставлены два выхода, а также мои ожидания. Я отредактирую свой пост, чтобы более четко объяснить свои ожидания   -  person Shiv Kumar    schedule 16.12.2012
comment
Если вы сделаете Accelerate виртуальным в VehicleBase и переопределите его в Sedan, вы получите ожидаемое поведение.   -  person juharr    schedule 16.12.2012
comment
@juharr, весь смысл этого вопроса в том, что метод является невиртуальным. Поэтому я не хочу делать это виртуальным.   -  person Shiv Kumar    schedule 16.12.2012
comment
@ShivKumar Вы либо делаете это в виртуальном мире, либо получаете поведение, которое видите, просто так. В 9 случаях из 10 использование нового метода — это просто плохой дизайн.   -  person juharr    schedule 16.12.2012
comment
@juharr, я согласен, что нужна очень веская причина, чтобы повторно ввести метод в классе-потомке. Точно так же наличие общедоступного абстрактного/виртуального метода также является плохим дизайном. Я не согласен с тем, что поведение, которое я вижу, понятно. Да, всегда можно просто согласиться с существующим положением вещей, оставить все как есть. Я просто думаю, что такое поведение сведет людей с ума.   -  person Shiv Kumar    schedule 16.12.2012


Ответы (2)


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

Вот почему вызов Accelerate идет к Sedan.Accelerate в контексте Main, но идет к VehicleBase.Accelerate в контексте VehicleBase.Drive:

В теле вашей функции Main вы объявили переменную типа Sedan и вызываете метод, используя эту переменную. Компилятор ищет метод с именем «Accelerate» в типе переменной, используемой для выполнения вызова, типа Sedan и находит Sedan.Accelerate.

Внутри метода VehicleBase.Drive тип "self" времени компиляцииVehicleBase. Это единственный тип, который компилятор может видеть в этом контексте исходного кода, поэтому вызов Accelerate в «VehicleBase.Drive» всегда будет иметь значение VehicleBase.Accelerate, даже если тип экземпляра объекта среды выполнения на самом деле Sedan.

В теле метода, объявленного в типе Sedan, компилятор разрешал вызов невиртуального метода, сначала просматривая методы типа Sedan, а затем просматривая тип VehicleBase, если не было найдено совпадений в Sedan.

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

Цели вызова невиртуальных методов выбираются в соответствии с типом времени компиляции без учета среды выполнения. Цели вызова виртуального метода выбираются в соответствии с типом экземпляра во время выполнения.

person dthorpe    schedule 17.12.2012
comment
спасибо за ваше объяснение и разъяснение причин, по которым VehicleBase.Accelerate вызывается из метода Drive. Ключом к тому, что я понял это, было то, что тип self/this времени компиляции является VehicleBase, а не типом переменной. Итак, хотя я теперь понимаю объяснение, я не совсем понимаю, почему компилятор не может видеть тип Sedan (внутри метода Drive) во время компиляции :). - person Shiv Kumar; 18.12.2012
comment
Будет ли Delphi вести себя так же? Я часто использовал диспетчеризацию статических методов в своих интересах и никогда не спотыкался об это. Возможно, я никогда повторно не вводил методы в классы-потомки, вызывая их из базового класса. А как насчет Java (вы знаете?) - person Shiv Kumar; 18.12.2012
comment
Единственная разница между поведением C# и Delphi заключается в том, что Delphi делала это за 6 лет до C#. ;› - person dthorpe; 18.12.2012
comment
В терминах Delphi подумайте об этом так: Delphi компилирует сверху вниз. Класс Sedan появляется после VehicleBase.Drive. Таким образом, компилятор еще даже не проанализировал класс Sedan, когда компилирует и выдает машинный код для VehicleBase.Drive. Так что, по крайней мере, для Delphi нет возможности узнать о классе Sedan в контексте метода VehicleBase.Drive. C# — многопроходный синтаксический анализатор, поэтому этот аргумент здесь не работает. - person dthorpe; 18.12.2012
comment
Таким образом, аргумент не работает .... поэтому такое поведение является преднамеренным. И я предполагаю, что это было и в Delphi? - person Shiv Kumar; 19.12.2012
comment
Да, это по дизайну. Повторно введенное имя метода доступно только тогда, когда оно доступно для типа переменной времени компиляции (включая self), используемой для выполнения вызова. - person dthorpe; 20.12.2012

Все это имеет смысл — когда вы объявляете транспортное средство как Sedan, два вызова Accelerate разрешаются по-разному:

  • Когда вызов Accelerate выполняется из метода Drive, он понятия не имеет, что в Sedan есть метод new, поэтому вызывает соответствующий метод базы
  • Когда вызов Accelerate выполняется из метода Main, компилятор знает, что вы вызываете метод new, поскольку он знает, что точный тип переменной vehicleSedan.

С другой стороны, когда вызов Accelerate сделан из метода Main, но переменная объявлена ​​как VehicleBase, компилятор не может предположить, что тип Sedan, поэтому он снова разрешает Accelerate в метод базового класса.

person Sergey Kalinichenko    schedule 15.12.2012
comment
поскольку Accelerate не является виртуальным методом, а переменная имеет тип Sedan, компилятор действительно знает, что метод был повторно представлен (новый), и должен вызвать повторно введенный метод Accelerate из оба места. - person Shiv Kumar; 16.12.2012
comment
@ShivKumar Компилятор знает это только тогда, когда вы объявляете переменную как Sedan, то есть так, как код написан в вашем сообщении. Однако, когда вы объявляете переменную как VehicleBase vehicle = new Sedan(), компилятор знает, что тип vehicle — это VehicleBase, поэтому он не может вызвать вновь добавленный метод. - person Sergey Kalinichenko; 16.12.2012
comment
Случай, когда переменная указана как седан, — это, на мой взгляд, неожиданное поведение. Так что, если во время компиляции он знает, что это тип Sedan, а рассматриваемый метод не является виртуальным, почему он ведет себя именно так? Может быть, если вы перечитаете мой пост еще раз, это поможет :) - person Shiv Kumar; 16.12.2012
comment
@ShivKumar Думаю, я правильно прочитал твой пост. Вы вызываете Accelerate из двух мест. В одном месте компилятор знает, что это Sedan; в другом месте компилятор не знает, что это Sedan. В частности, в Main компилятор точно знает, что это Sedan, но в VehicleBase.Drive компилятор не знает, что это Sedan. Как следствие, метод new вызывается только из Main, но не из метода Drive. - person Sergey Kalinichenko; 16.12.2012
comment
Я не понимаю, как компилятор не знает, что переменная имеет тип Sedan. Объявлено таковым. ИМХО, тут что-то другое. Тем не менее, мне трудно поверить, что это ожидаемое поведение. Это всегда можно рационализировать, но трудно поверить, что у нас есть такое поведение в языке, как задумано, потому что я не могу представить, где/когда я буду использовать это поведение. - person Shiv Kumar; 16.12.2012
comment
@ShivKumar Я не понимаю, как компилятор не знает, что переменная имеет тип Sedan. Объявлено таковым. Не к Drive методу VehicleBase, это не так! Для метода Drive переменная является неявной переменной this метода экземпляра, а не переменной vehicle метода Main. На самом деле Drive совершенно не подозревает, что класс Sedan вообще существует, не говоря уже о его методе Accelerate или переменной vehicle. Думайте о методе Drive как о отдельно скомпилированном модуле. Нет другого метода Accelerate, кроме как в VehicleBase. - person Sergey Kalinichenko; 16.12.2012
comment
Боюсь, вы не имеете смысла :) (для меня). Привод теперь по типу Седан (досталась по наследству). это тоже типа седан. Однако, поскольку методы (Drive и Accelerate) не являются виртуальными, компилятор должен иметь возможность определить, какой метод вызывать во время компиляции. Он должен отдавать предпочтение новому методу Accelerate, поскольку он объявлен в рассматриваемом типе. - person Shiv Kumar; 16.12.2012
comment
@ShivKumar Здесь, я полагаю, лежит ключ к вашему непониманию: Drive теперь относится к типу Sedan (унаследовано). Нет это не так! Drive остается в типе VehicleBase. Он оказывается доступным для Sedan в силу того, что он унаследован, но это ничего не меняет: метод Drive считает, что он выполняется в контексте VehicleBase, таким образом обращая внимание на переопределения только virtual функций. Если бы Accelerate был виртуальным, Drive обратил бы внимание на переопределение. Но поскольку это не так, Drive вызывает базовую версию Accelerate — единственную, о которой он знает. - person Sergey Kalinichenko; 16.12.2012