| Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате |
| Форум программистов > Общие вопросы по .NET и C# > Виртуальные методы |
| Автор: Xenon 14.11.2007, 22:04 |
| Даже почитав начало Рихтера не совсем понял как именно происходит вызов виртуальных методов именно на НИЗКОМ уровне в .NET. Я так понял там всю дорогу юзается GetType? Только где? |
| Автор: tol05 15.11.2007, 00:12 | ||
| Оо... Xenon, отличный вопрос...Для начала три тезиса: 1. Для вызова виртуального метода используется реальный тип объекта, а не тип ссылки, которой он присвоен. Соответственно для невиртуального метода используется тип ссылки, которая на него ссылается. 2. Каждый объект имеет ссылку на внутреннюю сруктуру данных (которая размещается в памяти домена) - т.н. объект-тип. А этот уже объект-тип имеет ссылку на таблицу методов (статических, виртуальных и обычных экземплярных, всех, которыми тип может оперировать)... Пользуясь случаем - еще раз спасибо Рихтеру за науку, дай Бог ему здоровья 3. для вызово невиртуальных методов используется директива call, для виртуальных - callvirt Ну а теперь все вместе. Допустим, есть
CLR должна найти метод F() и применить его к obj (допустим, что все проверки выполнились, все компилится и все допустимо - для простоты). Как найти метод? 1. obj - ссылка в стеке на область памяти (в куче), где расположены поля объекта. Нашли этот адрес и залезли в кучу. 2. По нужному адресу находим ссылку на объект-тип (Рихтер писал, что любой объект в куче имеет два члена - syncIndex и указатель на объект-тип). Идем по этому адресу 3. Находим эту служебную структуру и в ней отыскиваем таблицу методов этого типа. Тип у нас - Child и метод F() - переопределен. 4. Вызываем метод F виртуально. Если бы F() был не виртуальным методом, то вместо шага 2 (искать адрес объекта-типа в полях объекта) CLR просто взяла бы адрес объекта-типа ссылки на объект (т.е. Base). Вот и отличие callvirt от call. Вот так, вроде GetType() - как раз и указывает на объект-тип. дополнительно к Рихтеру советую почитать http://www.microsoft.com/Rus/Msdn/Magazine/2005/05/InsideNet.mspx |
| Автор: Xenon 15.11.2007, 00:51 | ||||||||
| Ну еще callvirt от call отличается проверкой ссылки на null. Да с syncIndex и т.д. все понятно. То есть получается, что, грубо говоря, для объекта вызывается GetType, узнается реальный тип, смотрится таблица методов по реальному типу, видим там override. Дальше что? Глядем в базовый? Нечетко я понимаю смысл "вызываем виртуально". Что конкретно происходит в случаях:
При
? Узнаем, чтo obj есть BaseC, допустим, видим там override у метода Foo. Значит "нужно вызывать виртуально". Значит мы берем статический тип - в моем случае BaseA и смотрим у него на метод Foo - он помечен как virtual, значит гуляем в производный (BaseB) - там, допустим, тоже virtual - значит идем в BaseC и попадаем в наш тип, там override, значит это Foo относящейся к этой группе методов Foo и вообще мы попали уже в BaseC, поэтому вызываем Foo из BaseC? А если бы у BaseB был бы метод Foo почен как override? Мы бы пошли опять же, поняли, что у нас obj типа BaseC а статический тип - BaseA. Пошли бы в BaseA - увидели бы virtual, стали бы искать в производном override метод, нашли бы его и успокоившись вызвали? Добавлено через 2 минуты Даже если то, что я написал не бред, то все равно интересно именно как CLR гуляет так по классам или это уже слишком мутно? |
| Автор: tol05 15.11.2007, 10:39 | ||||||||
Это как раз и значит, что по полю "object-type" объекта ищется объект-тип с нужной таблицей методов. Если бы "вызывали невиртуально", то не нужно было бы для нахождения таблицы методов выяснять real run-time type объекта, достаточно взять "захардкоданный" в твоем случае : никто никуда не гуляет
При запуске приложения метаданные типов считались и создались те пресловутые "внутренние структуры памяти", каждая - со своей таблицей методов. Когда компилировался код, то в местах вызовов Foo() была вставлена директива callvirt (а не call). Когда код работает, то при каждом callvirt ищется таблица методов (реального типа, а не типа ссылки), берется адрес входа в метод и метод выполняется. Ну а при создании кода метода JIT-ом - для каждого типа создается свой код и адрес точки входа в него записывается в таблицу методов наших трех типов. Вот из статьи, линк, на которую я давал
Одним словом : вся соль - в call и callvirt. P.S. можно еще вглубь двигаться, заметить, что компилятор может и невиртуальный метод вызывать как виртуальный, через callvirt (http://www.gotdotnet.ru/Forums/Common/288317.aspx), но думаю, что это не нужно. Это уже ньюансы реализации компилятора, оптимизаторов, контекстов использования. И это только запутает тему Рихтер "Вызов виртуальных методов, свойств и событий в CLR"
|
| Автор: Xenon 15.11.2007, 16:42 | ||
Все равно не понимаю. Получается мы просто получаем реальный тип и вызываем метод, который находим в его метаданных. Так почему тогда если в BaseA сделать метод не виртуальным вызовется Base.Foo()? Да даже если и виртуальный, то все равно, если в BaseB будет просто метод Foo, то мы спотыкнемся. На лицо какая-то связь в виде цепочки, поэтому я и предположил, что CLR гуляет вот так ...
Хорошо, метод переопределен - то есть в BaseB у нас virtual, а в BaseC override. Почему вызывается метод BaseA, если он вообще без меток стоит? |
| Автор: tol05 15.11.2007, 17:17 | ||||||||
если метод не виртуальный - для его нахождения используется тип ссылки
если виртуальный - то для его нахождения используется реальный тип созданного объекта
но объявления типов должно выглядеть так
в твоем же коде метод Foo() определен как виртуальный и в BaseA, и в BaseB. Т.е. это два разных метода. Поэтому компилятор и warning выбрасывает - он не знает, переопределить или скрыть Foo() ты хочешь. И цепочка полиморфизма BaseA-BaseC рвется. Подробно этот случай описан в ECMA 334, параграф 8.3 "Versioning" (http://www.ecma-international.org/publications/standards/Ecma-334.htm) Класс BaseC переопределяет Foo(), но только Foo() своего базового класса (класса BaseB). Полиморфизм все равно работает, но только для
|
| Автор: tol05 16.11.2007, 16:12 | ||||||||
| эпиграф "Как говорится кто о чем, а вшивый - о бане". Это снова я. "... Гляжу - а вдоль дороги - мертвые с косами стоят! И тишина ..." сам перечитал... результаты исследования памяти для варианта, сокращенно именуемого как (virtual virtual override)
результаты исследования памяти для варианта, сокращенно именуемого как (virtual override override)
Я делаю такие выводы:
В версии "virtual virtual override" определено два слота уже в классе BaseB. Причем адрес BaseА.Foo() не замещен, вместо этого добавлен слот BaseB.Foo() со своим адресом. Когда создается класс BaseC, то при создании его таблицы методов замещается адрес метода BaseB.Foo(), адрес метода BaseА.Foo() копируется, аналогично классу BaseB. При вызове Foo() объекта BaseC через ссылку на BaseА идет вызов маппинг на слот BaseА.Foo() и вызывается старая версия BaseА.Foo() При вызове Foo() объекта BaseC через ссылку на BaseB идет вызов маппинг на слот BaseB.Foo(), но при создании BaseC адрес BaseB.Foo() в его таблице методов был замещен и вызывается версия BaseC.Foo() Вот почему же при той же сигнатуре классов правильно работает
ну вот, теперь ИМХО все. |