class Base
{
//...
}
class Derived: Base
{
//...
}
И, конечно можно ссылке на базовый класс присвоить ссылку на объект производного класса
Derived der1 = new Derived();
Base base1 = der1;
Ну, а потом, можно так:
Derived der2 = base1;
А вот так не "прокатит":
Base base2 = new Base();
Derived der3 = base2;
Т.к base2 не является ссылкой на объект типа Derived
Вопрос: как вообще объект базового класс может "знать", что указывает на более расширенный класс? Он, как бы, должен быть расширенным классом, чтобы знать об этом...
Здравствуйте, Qbit86, Вы писали:
Q>Здравствуйте, zfima, Вы писали:
Z>>Ну, а потом, можно так: Z>>Derived der2 = base1;
Q>Ты это проверял?
Извеняюсь, забыл кастинг:
Derived der2 = (Derived)base1;
Z>>Derived der3 = base2;
Q>Компилятор эту строчку будет трактовать так же, как и предыдущую, независимо от контекста.
Трактует то конечно, может и одинакого, но во время исполнения первый вариант работает а второй нет
Здравствуйте, Qbit86, Вы писали:
Q>Здравствуйте, zfima, Вы писали:
Z>>Трактует то конечно, может и одинакого, но во время исполнения первый вариант работает а второй нет
Q>Если ты хорошо подумаешь, то поймёшь, что твоя исходная формулировка эквивалентна следующей:
Q>
Q>Почему эти две команды Console.WriteLine выводят разные значения?
Q>
Q>Base base1 = new Derived();
Q>Console.WriteLine(base1.GetType());
Q>Base base2 = new Base();
Q>Console.WriteLine(base2.GetType());
Q>
Здравствуйте, zfima, Вы писали:
Z>Трактует то конечно, может и одинакого, но во время исполнения первый вариант работает а второй нет
Есть объект определенного типа, а есть ссылка на этот объект.
В первом варианте base1 ссылается на объект типа Derived, а во втором base2 ссылается на объект типа Base. Поэтому такой результат.
Явное приведение — это всего лишь команда вида "проверь, является ссылается ли ссылка на объект заданного типа, и, если нет, выбрось исключение". С ее помощью из объекта типа Base объект типа Derived не сделать, если вопрос об этом.
Здравствуйте, zfima, Вы писали:
Q>>Чтобы понять, как это работает, необходимо иметь представление о таблице виртуальных методов.
Z>Будем апгрэйдиться!
Лучший способ проникнуться полиморфизмом-на-основе-позднего-связывания — это реализовать его вручную. Возьми язык Си (или С++/C#, но без использования слова virtual, только указатели на функции или делегаты) и реализуй на нём пример полиморфного поведения. У тебя будет один класс — пачка функций, будет несколько его предопределённых неизменяемых экземпляров (по числу классов в иерархии), проинициализированных нужными функциями. Классы твоей доменной области будут инициализироваться ссылкой на эту таблицу функций; разные экземпляры одного класса будут инициализироваться ссылкой на одну и ту же общую таблицу.
Когда всё это проделаешь, станет очевидным, что слово virtual является по большему счёту лишь синтаксическим сахаром и дополнительной проверкой типов.
Глаза у меня добрые, но рубашка — смирительная!
Re: Базовый вопрос по наследованию
От:
Аноним
Дата:
04.02.11 12:14
Оценка:
Объект всегда знает какого он типа (obj.GetType())
Здравствуйте, zfima, Вы писали:
Z>Вопрос: как вообще объект базового класс может "знать", что указывает на более расширенный класс? Он, как бы, должен быть расширенным классом, чтобы знать об этом...
Во-первых, каждый класс в управляемой куче имеет некоторый заголовок, хранящий, помимо прочих служебных полей, указатель на специальный объект типа RuntimeType, который называется "type object". Этот указатель в заголовке каждого объекта инициализируется при создании объекта. Для каждого класса создается только один "type object". И каждый экземпляр одного и того же класса будет содержать в себе указатель на один и тот же "type object" для этого конкретного класса. Объект "type object" содержит в себе статические поля соответствующего класса, таблицу методов и прочие служебные поля. Нас же интересует таблица методов. Таблица методов содержит слоты для всех методов, реализованных конкретным классом, а так же слоты виртуальных методов, унаследованных от родителей (даже если класс их не переопределяет). Каждый слот таблицы методов содержит указатель на адрес в памяти, где находится некоторый код. Изначально, там находятся инструкции вызывающие компиляцию метода в нативный код и последующее его выполнение, но после компиляции метода на место этих инструкций записывается инструкция перехода (jmp) на код, содержащий тело уже скомпилированного в нативный код метода.
Вернемся чуть-чуть назад. Во время выполнения программы при первом обращении к классу (не объекту, а классу) создается соответствующий "type object" и заполняются слоты таблицы методов.
Теперь рассмотрим 3 основных случая: вызов невиртуального метода, вызов виртуального метода и вызов интерфейсного метода.
1) В случае вызова невиртуального метода происходит обращение к обьекту "type object" соответствующему типу переменной (не реальному типу объекта), т.е. для кода
Base b = new Derived();
b.SomeMethod(); // SomeMethod is not virtual
при jit-компиляции этого кода jit-компилятор обратится к Base Type Object и проверит его таблицу методов на содержание там метода SomeMethod(). Если метод SomeMethod() найден в таблице методов для типа Base, то jit-компилятор возьмет его адрес (сначала скомпилировов, если он еще не был скомпилирован в нативный код) и запишет инструкцию call с этим адресом. Если метод SomeMethod() не найден в таблице методов для типа Base, то jit-компилятор произведет поиск вверх по иерархии по направлению к типу Object пока не найдет этот метод. Таким образом будет подготовлен и произведен вызов невиртуального метода.
2) В случае вызова виртуального метода
Base b = new Derived();
b.SomeVirtualMethod();
jit-компилятор создаст чуть более сложный код. Этот код будет получать указатель на реальный "type object" для нашего объекта, т.е. для данного примера указатель на Derived Type Object, далее будет идти обращение к таблице методов для типа Derived, получение адреса метода SomeVirtualMethod() и вызов метода по этому адресу. Если метод SomeVirtualMethod() не переопределен в типе Derived, то слот SomeVirtualMethod в таблице методов типа Derived будет указывать на то же самое место, что и слот SomeVirtualMethod в таблице методов типа Base. Вот так происходит подготовка и вызов виртуального метода.
3) В случае вызова интерфейсного метода
IBase ib = new Derived();
ib.SomeInterfaceMethod();
jit-компилятор создаст еще чуть более сложный код, хотя механизм похож на вызов виртуального метода. Дело в том, что в таблице методов есть ссылка на еще одну таблицу — таблицу интерфейсных методов (ну это я ее так называю, в разных источниках она по-разному называется, например Interface Offset Table или Domain Interface VTable Map). Каждой реализации интерфейса будет соответствовать один слот в этой таблице интерфейсных методов. Каждый слот в свою очередь будет указывать обратно на таблицу методов, а точнее на первый слот интерфейсного метода данного интерфейса (см. картинку ниже чтобы понять эту кашу). Таким образом, при вызове интерфейсного метода добавляются (по сравнению с вызовом виртуального метода) инструкции получения сначала адреса таблицы интерфейсных методов, а потом адреса слота в таблице методов типа.
Здравствуйте, MozgC, Вы писали:
MC>Здравствуйте, zfima, Вы писали:
Z>>Вопрос: как вообще объект базового класс может "знать", что указывает на более расширенный класс? Он, как бы, должен быть расширенным классом, чтобы знать об этом...
MC>Во-первых, каждый класс в управляемой куче имеет некоторый заголовок, хранящий, помимо прочих служебных полей, указатель на специальный объект типа RuntimeType, который называется "type object". Этот указатель в заголовке каждого объекта инициализируется при создании объекта. Для каждого класса создается только один "type object". И каждый экземпляр одного и того же класса будет содержать в себе указатель на один и тот же "type object" для этого конкретного класса. Объект "type object" содержит в себе статические поля соответствующего класса, таблицу методов и прочие служебные поля. Нас же интересует таблица методов. Таблица методов содержит слоты для всех методов, реализованных конкретным классом, а так же слоты виртуальных методов, унаследованных от родителей (даже если класс их не переопределяет). Каждый слот таблицы методов содержит указатель на адрес в памяти, где находится некоторый код. Изначально, там находятся инструкции вызывающие компиляцию метода в нативный код и последующее его выполнение, но после компиляции метода на место этих инструкций записывается инструкция перехода (jmp) на код, содержащий тело уже скомпилированного в нативный код метода. MC>Вернемся чуть-чуть назад. Во время выполнения программы при первом обращении к классу (не объекту, а классу) создается соответствующий "type object" и заполняются слоты таблицы методов. MC>Теперь рассмотрим 3 основных случая: вызов невиртуального метода, вызов виртуального метода и вызов интерфейсного метода.
MC>1) В случае вызова невиртуального метода происходит обращение к обьекту "type object" соответствующему типу переменной (не реальному типу объекта), т.е. для кода MC>
Base b = new Derived();
MC>b.SomeMethod(); // SomeMethod is not virtual
MC>при jit-компиляции этого кода jit-компилятор обратится к Base Type Object и проверит его таблицу методов на содержание там метода SomeMethod(). Если метод SomeMethod() найден в таблице методов для типа Base, то jit-компилятор возьмет его адрес (сначала скомпилировов, если он еще не был скомпилирован в нативный код) и запишет инструкцию call с этим адресом. Если метод SomeMethod() не найден в таблице методов для типа Base, то jit-компилятор произведет поиск вверх по иерархии по направлению к типу Object пока не найдет этот метод. Таким образом будет подготовлен и произведен вызов невиртуального метода.
MC>2) В случае вызова виртуального метода MC>
Base b = new Derived();
MC>b.SomeVirtualMethod();
MC>jit-компилятор создаст чуть более сложный код. Этот код будет получать указатель на реальный "type object" для нашего объекта, т.е. для данного примера указатель на Derived Type Object, далее будет идти обращение к таблице методов для типа Derived, получение адреса метода SomeVirtualMethod() и вызов метода по этому адресу. Если метод SomeVirtualMethod() не переопределен в типе Derived, то слот SomeVirtualMethod в таблице методов типа Derived будет указывать на то же самое место, что и слот SomeVirtualMethod в таблице методов типа Base. Вот так происходит подготовка и вызов виртуального метода.
MC>3) В случае вызова интерфейсного метода MC>
IBase ib = new Derived();
MC>ib.SomeInterfaceMethod();
MC>jit-компилятор создаст еще чуть более сложный код, хотя механизм похож на вызов виртуального метода. Дело в том, что в таблице методов есть ссылка на еще одну таблицу — таблицу интерфейсных методов (ну это я ее так называю, в разных источниках она по-разному называется, например Interface Offset Table или Domain Interface VTable Map). Каждой реализации интерфейса будет соответствовать один слот в этой таблице интерфейсных методов. Каждый слот в свою очередь будет указывать обратно на таблицу методов, а точнее на первый слот интерфейсного метода данного интерфейса (см. картинку ниже чтобы понять эту кашу). Таким образом, при вызове интерфейсного метода добавляются (по сравнению с вызовом виртуального метода) инструкции получения сначала адреса таблицы интерфейсных методов, а потом адреса слота в таблице методов типа.
MC>
Здравствуйте, MozgC, Вы писали:
MC>1) В случае вызова невиртуального метода происходит обращение к обьекту "type object" соответствующему типу переменной (не реальному типу объекта), т.е. для кода MC>
Base b = new Derived();
MC>b.SomeMethod(); // SomeMethod is not virtual
MC>при jit-компиляции этого кода jit-компилятор обратится к Base Type Object и проверит его таблицу методов на содержание там метода SomeMethod(). Если метод SomeMethod() найден в таблице методов для типа Base, то jit-компилятор возьмет его адрес (сначала скомпилировов, если он еще не был скомпилирован в нативный код) и запишет инструкцию call с этим адресом. Если метод SomeMethod() не найден в таблице методов для типа Base, то jit-компилятор произведет поиск вверх по иерархии по направлению к типу Object пока не найдет этот метод. Таким образом будет подготовлен и произведен вызов невиртуального метода.
По поводу выделенной фразы — это точно? Признаться, есть некоторые сомнения. Просто не вижу необходимости возлагать это на JIT, так как вся необходимая информация доступна уже на этапе компиляции в IL, по-моему
Здравствуйте, Jolly Roger, Вы писали:
MC>>1) В случае вызова невиртуального метода происходит обращение к обьекту "type object" соответствующему типу переменной (не реальному типу объекта), т.е. для кода MC>>
Base b = new Derived();
MC>>b.SomeMethod(); // SomeMethod is not virtual
MC>>при jit-компиляции этого кода jit-компилятор обратится к Base Type Object и проверит его таблицу методов на содержание там метода SomeMethod(). Если метод SomeMethod() найден в таблице методов для типа Base, то jit-компилятор возьмет его адрес (сначала скомпилировов, если он еще не был скомпилирован в нативный код) и запишет инструкцию call с этим адресом. Если метод SomeMethod() не найден в таблице методов для типа Base, то jit-компилятор произведет поиск вверх по иерархии по направлению к типу Object пока не найдет этот метод. Таким образом будет подготовлен и произведен вызов невиртуального метода.
JR>По поводу выделенной фразы — это точно? Признаться, есть некоторые сомнения. Просто не вижу необходимости возлагать это на JIT, так как вся необходимая информация доступна уже на этапе компиляции в IL, по-моему
CLR via C#, 3rd edition, p.108:
void M3() {
Employee e = new Manager();
year = e.GetYearsEmployed();
...
}
The next line of code in M3 calls Employee’s nonvirtual instance GetYearsEmployed method. When calling a nonvirtual instance method, the JIT compiler locates the type object that corresponds
to the type of the variable being used to make the call. In this case, the variable e is defined as an Employee. (If the Employee type didn’t define the method being called, the JIT compiler walks down the class hierarchy toward Object looking for this method. It can do this because each type object has a field in it that refers to its base type; this information is not shown in the figures.) Then, the JIT compiler locates the entry in the type object’s method table that refers to the method being called, JITs the method (if necessary), and then calls the JITted code.
Здравствуйте, MozgC, Вы писали:
MC>CLR via C#, 3rd edition, p.108:
MC>[q]
void M3() {
MC> Employee e = new Manager();
MC> year = e.GetYearsEmployed();
MC> ...
MC>}
MC>(If the Employee type didn’t define the method being called, the JIT compiler walks down the class hierarchy toward Object looking for this method.
И всё-же это моих сомнений не развеяло Может я неправильно понимаю IL, но вот такой код
static int y;
public void Test()
{
DerivedObj obj = new DerivedObj()
var y1 = obj.Mul(2, 4);
var y2 = obj.Sum(5, 10);
y = y1 + y2;
}
}
public class BaseObj
{
public int Sum(int x1, int x2)
{
return x1 + x2;
}
}
public class DerivedObj : BaseObj
{
public int Mul(int x1, int x2)
{
return x1 * x2;
}
}
Здравствуйте, Jolly Roger, Вы писали:
JR>Т.е. сразу указан вызов BaseObj::Sum Да и здравый смысл подсказывает, что возлагать поиск невиртуального метода на JIT как-бы не очень экономно
А какие есть предложения по поводу того, кто же будет искать адрес метода CarpetCleaner.BaseObj::Sum более экономно?
Здравствуйте, samius, Вы писали:
S>А какие есть предложения по поводу того, кто же будет искать адрес метода CarpetCleaner.BaseObj::Sum более экономно?
Ну во-первых, всё-таки сразу имеет место обращение к типу BaseObj, без какого-либо поиска в DerivedObj и прохода по списку наследования, о чём говорилось здесь
. А во-вторых, зачем вообще что-то искать? Точка входа интересующего метода известна на этапе компиляции, искать её во время выполнения нет нужды. Можно просто подставить адрес этой точки в место вызова. Компиляторы неуправляемых языков так и поступают, а в случае dotnet разница, в принципе, должна быть только в том, что в этой точке до первого обращения будет находиться jmp на JIT, а после — код самого метода. По-моему так
Здравствуйте, Jolly Roger, Вы писали:
JR>Ну во-первых, всё-таки сразу имеет место обращение к типу BaseObj, без какого-либо поиска в DerivedObj и прохода по списку наследования, о чём говорилось здесь
. А во-вторых, зачем вообще что-то искать? Точка входа интересующего метода известна на этапе компиляции, искать её во время выполнения нет нужды. Можно просто подставить адрес этой точки в место вызова. Компиляторы неуправляемых языков так и поступают, а в случае dotnet разница, в принципе, должна быть только в том, что в этой точке до первого обращения будет находиться jmp на JIT, а после — код самого метода. По-моему так
У меня такой вопрос: а как JIT узнает адрес объекта "type object" для базового типа, в котором реально определен метод? В рантайме же у JIT'а будет иметься только объект дочернего типа, и ссылка на его type object. Поэтому JIT'у придется идти по иерархии, чтобы найти type object для базового типа, чтобы уже обратиться к method table базового типа и вызвать этот метод. Тогда может Рихтер прав?
Или JIT может более коротким путем выйти на type object для базового типа в котором реализован метод?
Здравствуйте, Jolly Roger, Вы писали:
JR>Здравствуйте, samius, Вы писали:
S>>А какие есть предложения по поводу того, кто же будет искать адрес метода CarpetCleaner.BaseObj::Sum более экономно?
JR>Ну во-первых, всё-таки сразу имеет место обращение к типу BaseObj, без какого-либо поиска в DerivedObj и прохода по списку наследования, о чём говорилось здесь
.
Тут вопрос скорее того, нафига нужен callvirt для вызова невиртуальных методов, и рассчитывает ли JIT на то что искомый метод в точности указана в IL...
JR>А во-вторых, зачем вообще что-то искать? Точка входа интересующего метода известна на этапе компиляции, искать её во время выполнения нет нужды.
Во время выполнения не ищется, ищется во время JIT компиляции JR>Можно просто подставить адрес этой точки в место вызова. Компиляторы неуправляемых языков так и поступают, а в случае dotnet разница, в принципе, должна быть только в том, что в этой точке до первого обращения будет находиться jmp на JIT, а после — код самого метода. По-моему так
Но вообще говоря в IL указан callvirt, потому вполне мог подразумеваться Derived::Sum, если таковой есть
Здравствуйте, MozgC, Вы писали:
MC>У меня такой вопрос: а как JIT узнает адрес объекта "type object" для базового типа, в котором реально определен метод?
Кто такой "type object"? И зачем он нужен? JIT-у достаточно знать метод, который надо вызвать, а этот метод в явном виде присутствует в IL-е.
На всякий случай, не забывайте, что метод у объекта — это простая "процедура", принимающая дополнительный параметр.
MC>В рантайме же у JIT'а будет иметься только объект дочернего типа, и ссылка на его type object.
У JIT-а нет никакого объекта, он не знает с каким именно объектом будет вызван метод. На вход он получает только IL.
Здравствуйте, MozgC, Вы писали:
MC>Здравствуйте, samius, Вы писали:
S>>Тут вопрос скорее того, нафига нужен callvirt для вызова невиртуальных методов
MC>Why does C# always use callvirt?
Выглядит нелепо, т.к. я еще застал те времена, когда вызов невиртуального метода у null прокатывал. А общение с дотнетом я начал году в 2003-м. Когда конкретно застал такое поведение — точно не помню, но сейчас уже не канает.