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

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

Посмотрите на код ниже.

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");
    }
}

Консоль Windows показывает следующее:

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

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

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

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

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

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

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,

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

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

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

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

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

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