Skip to content

Подробно о виртуальных методах

edited March 2022 in Tutorials

Для начала небольшой пример:

Bar b=new Bar();
b.Test(); //At Bar
Foo f=b;
f.Test(); //At Bar

class Foo{
    public virtual Test(){WriteLine("At Foo");}
}

class Bar:Foo{
    public override Test(){WriteLine("At Bar");}
}

Видим, что f.Test() - это вызов Test() из Bar, а не из Foo, хотя переменная f типа Foo. Это потому, что если метод помечен модификатором virtual, то каждый раз при обращении к нему производится поиск override -метода среди всех имеющихся у объекта. Если такой метод найден, то вызывается он, а если нет, то сам виртуальный. Ниже я подробно и, надеюсь, понятно расскажу, как происходит вызов виртуальных методов.
Когда я был начинающим программистом, я не очень хорошо понимал, как работают виртуальные методы. Это было из-за неправильного представления о наследовании методов. А ещё из-за непонимания того, что тип переменной и реальный тип объекта, который в ней хранится могут быть разными.
Итак, в OverScript наследование заключается в копировании в класс всех членов из родительского класса. Я пишу про OverScript, но это в той или иной мере справедливо и для других языков. В примере выше в классе Bar оказывается два метода Test. Если у одного метода есть несколько вариантов, то вызывается последний. Далее по шагам:
1. В первой строке создаётся экземпляр Bar, у которого один Test() из Foo, а другой из Bar.
2. Во второй строке происходит вызов Test(), но это не прямой вызов, а виртуальный, т.е. сначала происходит поиск нужного метода. Почему - напишу далее.
3. В третьей строке переменной типа Foo присваивается экземпляр Bar. Это возможно, т.к. Bar наследует от Foo. Простыми словами: переменная родительского типа может принимать объекты производных типов. В переменной f теперь лежит объект типа Bar (экземпляр класса Bar).
4. В четвёртой происходит вызов виртуального метода. Казалось бы, раз f типа Foo, значит должна вызываться Test() из Foo, но модификатор virtual указывает интерпретатору, что нужно произвести поиск override-метода. Этот поиск делается при каждом обращении к virtual или override методу. К override потому, что он сам тоже может быть перекрыт другим override-методом.

Как интерпретатор ищет нужный метод

Интерпретатор видит в экземпляре Bar (а переменная f ссылается именно на него, хоть и задана как Foo) методы в обратном порядке:

public override Test(){WriteLine("At Bar");}
public virtual Test(){WriteLine("At Foo");}

Он проходит от первого до последнего и, если метод помечен как override, то следующий метод заменяется на него. Этот следующий метод должен быть помечен как virtual или override, и не как sealed. Иначе при загрузке скрипта вылетит ошибка. В результате список методов будет такой:

public override Test(){WriteLine("At Bar");}
public override Test(){WriteLine("At Bar");}

Интерпретатор вызовет первый (т.е. последний в коде).
Теперь давайте сделаем Test() в Bar не override, а virtual.

Bar b=new Bar();
Foo f=b;
f.Test(); //At Foo

class Foo{
    public virtual Test(){WriteLine("At Foo");}
}

class Bar:Foo{
    public virtual Test(){WriteLine("At Bar");} //эта функция перекрывает Test из Foo, но не заменяет её в Foo
}

Интерпретатор видит:

public virtual Test(){WriteLine("At Bar");}
public virtual Test(){WriteLine("At Foo");}

Получается, что первая - это Test() из Bar. Но интерпретатор отбрасывает методы, которые не в Foo или унаследованных Foo классах. Т.е. методы Bar отбрасываются и остаётся одна Test() из Foo. То же самое было бы и без модификатора virtual.
Посмотрим на следующий пример:

Baz b=new Baz();
Bar br=b;
br.Test(); //At Foo //а если Test() в Baz пометить override, то будет выведено: At Baz

class Foo{
    public virtual Test(){WriteLine("At Foo");}
}

class Bar:Foo{}

class Baz:Bar{
    public Test(){WriteLine("At Baz");}
}

В Bar своего Test() нет, а есть из Foo. Test() в Baz без override, поэтому она не подменит Test() из Foo.
Когда я говорю про подмену метода, я имею в виду замену в списке всех доступных методов, который составляется при каждом вызове виртуального метода. Т.е. при загрузке скрипта override не заменяет методы в его реальной структуре, а действует только при вызове virtual/override методов, и так же никаких постоянных замен методов не делает. Вот пример, демонстрирующий это:

Foo f=new Foo();
f.Test(); //At Foo

class Foo{
    public virtual Test(){WriteLine("At Foo");}
}

class Bar:Foo{
    public override Test(){WriteLine("At Bar");}
}

В этом примере Test() в Bar никак не влияет на Test() в Foo потому, что f ссылается на экземпляр Foo, в котором есть только один свой метод. Если вместо new Foo() написать new Bar(), то было бы два метода, и второй заменил бы первый. Ещё раз повторюсь: override влияет только на поиск метода при его вызове, а не меняет методы в коде (структуре программы).

Для закрепления посмотрим ещё пример, в котором видно, что override-метод может быть подменён другим override-методом:

Baz b=new Baz();
b.Test(); //At Baz
(Bar\b).Test(); //At Baz
(Foo\b).Test(); //At Baz

class Foo{
    public virtual Test(){WriteLine("At Foo");}
}

class Bar:Foo{
    public override Test(){WriteLine("At Bar");}
}

class Baz:Bar{
    public override Test(){WriteLine("At Baz");}
}

Интерпретатор видит, что объект в переменной b это экземпляр класса Baz, в котором есть следующие методы (в обратном порядке):

public override Test(){WriteLine("At Baz");}
public override Test(){WriteLine("At Bar");}
public virtual Test(){WriteLine("At Foo");}

Интерпретатор идёт по списку сверху вниз. Первый метод с override, а значит интерпретатор заменяет им следующий за ним метод.

public override Test(){WriteLine("At Baz");}
public override Test(){WriteLine("At Baz");}
public virtual Test(){WriteLine("At Foo");}

Теперь второй метод, который был подменён первым. Он с override, и происходит то же самое, что на предыдущем шаге.

public override Test(){WriteLine("At Baz");}
public override Test(){WriteLine("At Baz");}
public override Test(){WriteLine("At Baz");}

Теперь все три метода - это один и тот же метод из Baz. Вызывается первый в списке. И так при всех вызовах: b.Test(), (Bar\b).Test(), (Foo\b).Test().
Выше я писал про отбрасывание методов из классов, которые не относятся к типу, с которым делается вызов.

Baz b=new Baz();
b.Test(); //это вызов с типом Baz 
(Bar\b).Test(); //вызов с типом Bar
(Foo\b).Test(); //вызов с типом Foo

Так вот: когда интерпретатор проходит по списку и находит родной метод, то отбрасывает все перед ним (если не находит, то не отбрасывает). Родной метод - это метод класса (или из унаследованных), в контексте которого делается вызов (вызов с типом).
В примере выше ничего не отбрасывается. Когда вызывается b.Test(), то все три метода - родные (они все из Baz, ну т.е. это один и тот же метод, который подменил собой остальные). Когда (Bar\b).Test() и (Foo\b).Test(), то все три неродные, но т.к. родного нет, то отбрасывания не происходит (значит родной подменён через override).
Ещё хочу сказать про exclusive-методы. При загрузке скрипта они не копируются в производные классы.

Baz b=new Baz();
b.Test(); //At Baz
(Bar\b).Test(); //At Bar //это прямой, а не виртуальный вызов
(Foo\b).Test(); //At Baz

class Foo{
    public virtual Test(){WriteLine("At Foo");}
}

class Bar:Foo{
    public exclusive Test(){WriteLine("At Bar");}
}

class Baz:Bar{
    public override Test(){WriteLine("At Baz");}
}

В этом случае у экземпляра Baz есть только два метода - из Baz и Foo, а из Bar нет, т.к. он эксклюзивный. Если убрать модификатор exclusive, то при загрузке скрипта вылетит ошибка потому, что Test() в Baz помечен как override, а метод для подмены не помечен как virtual/override.

Как внутри интерпретатора устроен вызов обычных и виртуальных методов

Обычный (прямой) вызов метода - это вызов делегата, в котором есть ссылка на функцию, которую нужно выполнить. Эта функция находится во время загрузки скрипта.
Виртуальный вызов - это тоже вызов делегата, но в котором нет ссылки на функцию, а каждый раз производится её поиск с учётом типа переменной, в которой находится объект (экземпляр). Поиск занимает время, поэтому виртуальные вызовы медленные. Для ускорения вызова virtual/override методов делается кэширование результатов поиска.

Sign In or Register to comment.