Различие переопределения и сокрытия методов. Полиморфизм в Java. Динамическое и статическое связывание. Инициализация объектов. Поведение полиморфных методов при вызове из конструкторов
Применение рефлексии, позднего связывания и атрибутов
В данной статье я предлагаю посмотреть комплексный пример использования рефлексии, позднего связывания и атрибутов. Давайте предположим, что была поставлена задача разработать так называемое расширяемое приложение, к которому можно было бы подключать сторонние инструменты.
Что именно подразумевается под расширяемым приложением? Рассмотрим IDE-среду Visual Studio 2010. При разработке в этом приложении были предусмотрены специальные "ловушки" (hook) для предоставления другим производителям ПО возможности подключать свои специальные модули. Понятно, что разработчики Visual Studio 2010 не могли добавить ссылки на несуществующие внешние сборки.NET (т.е. воспользоваться ранним связыванием), тогда как же им удалось обеспечить в приложении необходимые методы-ловушки? Ниже описан один из возможных способов решения этой проблемы.
Во-первых, любое расширяемое приложение должно обязательно обладать каким-нибудь механизмом для ввода, который даст возможность пользователю указать подключаемый модуль (например, диалоговым окном или соответствующим флагом командной строки). Это требует применения динамической загрузки.
Во-вторых, любое расширяемое приложение должно обязательно быть способным определять, поддерживает ли модуль функциональные возможности (например, набор интерфейсов), необходимые для его подключения к среде. Это требует применения рефлексии.
В-третьих, любое расширяемое приложение должно обязательно получать ссылку на требуемую инфраструктуру (например, набор типов интерфейсов) и вызывать ее члены для приведения в действие лежащих в ее основе функций. Это может требовать применения позднего связывания.
Если расширяемое приложение изначально программируется так, чтобы запрашивать определенные интерфейсы, оно получает возможность определять во время выполнения, может ли активизироваться интересующий тип, и после успешного прохождения типом такой проверки позволять ему поддерживать дополнительные интерфейсы и получать доступ к их функциональным возможностям полиморфным образом. Именно такой подход и предприняли разработчики Visual Studio 2010, причем ничего особо сложного в нем нет.
В первую очередь необходимо создать сборку с типами, которые должна обязательно использовать каждая оснастка, чтобы иметь возможность подключаться к расширяемому приложению. Для этого создадим проект типа Class Library (Библиотека классов), и определим в нем два следующих типа:
Using System; namespace PW_CommonType { public interface IApplicationFunc { void Go(); } public class InfoAttribute: System.Attribute { public string CompanyName { get; set; } public string CompanyUrl { get; set; } } }
Далее потребуется создать тип, реализующий интерфейс IApplicationFunc. Чтобы не усложнять пример создания расширяемого приложения, давайте сделаем этот тип простым. Создадим новый проект типа Class Library на C# и определим в нем тип класса по имени MyCompanyInfo:
Using System; using PW_CommonType; using System.Windows..Go() { MessageBox.Show("Важная информация!"); } } }
И, наконец, последний шаг заключается в создании самого расширяемого приложения Windows Forms, которое позволит пользователю выбирать желаемую оснастку с помощью стандартного диалогового окна открытия файлов Windows.
Теперь нужно добавить в него ссылку на сборку PW_CommonType.dll, но не на библиотекy кода CompanyInfo.dll. Кроме того, необходимо импортировать в главный файл кода формы (для его открытия щелкните правой кнопкой мыши в визуальном конструкторе формы и выберите в контекстном меню пункт View Code (Просмотреть код)) пространства имен System.Reflection и PW_CommonType. Вспомните, что цель создания данного приложения состоит в том, чтобы увидеть, как использовать позднее связывание и рефлексию для проверки отдельных двоичных файлов, создаваемых другими производителям, на предмет их способности выступать в роли подключаемых оснасток.
I. Основные принципы ООП
1. Инкапсуляция – принцип ООП, который заключается в объединении в единое целое данных и алгоритмов их обработки.
Данные объектав ООП называются полями объекта , а алгоритмы, т. е. действия над данными объекта,называются методами объекта , которые оформляются в виде подпрограмм.
2. Наследование – принцип ООП, который заключается в свойстве объектов порождать своих потомков.
Объект-потомок автоматически наследует от родителя все поля и методы, может дополнять объекты новыми полями и заменять или дополнять методы родителя.
3. Полиморфизм – это свойство объектов, имеющих одного общего родителя, решать разными способами схожие по смыслу задачи.
Полиморфизм методов выражается в наличии у предка и потомка методов с одинаковыми именами, имеющих разную реализацию.
II. Структура объявления объектного типа
<имя_типа>=OBJECT
<имя_поля>:<тип_поля>;
<методы>;
После определения типа объекта должны следовать описания всех методов, перечисленных в объектном типе, которые представляют собой тексты процедур и функций. Отличие от обычного описания заключается в том, что имя подпрограммы , являющейся методом объекта, указываемое в её заголовке, состоит из 2 частей:
<имя_типа>.<имя_подпрограммы>
III. Свойство наследования
родительским типом , от которого наследуются все поля и методы. Для того, чтобы произошло наследование, в определение объектного типа после OBJECT в скобках необходимо указать имя родительского типа.
Правила наследования.
1. В определении типа наследника не должно быть полей, совпадающих по именам с полями родителя; имена методов у наследников и родителя могут совпадать – в этом проявляется свойство полиморфизма .
2. При построении объектного типа наследника, в первую очередь, наследуются поля родителя, затем добавляются поля наследника; после полей наследуются все методы родителя, затем добавляются методы наследника, если их имена не совпадают с именами методов родителя; если совпадения существуют, то это означает, что методы наследника являются полиморфными методами и замещают одноименные методы предка.
Свойство полиморфизма.
Свойство полиморфизма заключается в том, что в определении типа наследника могут быть методы, одноименные с именами методов родителя.
Правила наследования при полиморфизме:
· одноименные методы наследника заменяют методы родителя;
· методы наследника, не совпадающие по именам с методами родителя, добавляются после методов родителя.
IV. Виртуальные методы
Методы объекта являются статическими методами , если компилятор размещает их и разрешает все ссылки на них во время компиляции и компоновки. Процесс, с помощью которого вызовы статических методов однозначно разрешаются компилятором во время компиляции, называется ранним связыванием .
При позднем связываниивызывающий и вызываемый методы не могут быть связаны по адресам во время компиляции, поэтому включается механизм, позволяющий выполнить связывание в тот момент, когда вызов действительно произойдет, т. е. во время выполнения программы.
Виртуальные методы и таблица виртуальных методов (ТВМ).
Позднее связывание реализуется с помощью виртуальных методов , признаком которых является наличие ключевого слова VIRTUAL после заголовка метода в определении объектного типа.
Позднее связывание осуществляется с помощью таблиц виртуальных методов (ТВМ), которые строятся компилятором в сегменте данных программы для всех виртуальных методов, описанных в программе. ТВМ представляет собой таблицу адресов процедур, которые являются виртуальными методами. Для любого объекта, содержащего виртуальные методы, требуется, чтобы в памяти находился экземпляр таблицы виртуальных методов.Для каждого объектного типа строится только одна ТВМ. Указатель на таблицу виртуальных методов автоматически заносится в специальное поле, которое имеется в каждом экземпляре объектного типа при выполнении конструктора. Обращение к ТВМ через этот указатель происходит при каждом вызове виртуального метода.
Конструктор.
метод-конструктор
Конструктор – это специальный вид процедуры, которая выполняет установочную работу для обеспечения позднего связывания. Все типы объектов, имеющих виртуальные методы, должны содержать конструктор, который всегда вызывается до первого вызова виртуального метода. Вызов виртуального метода без предшествующего обращения к конструктору может вызвать блокировку системы.
Конструктором может быть только процедура, в заголовке которой вместо служебного слова PROCEDURE необходимо написать CONSTRUCTOR.
Конструктор не может быть виртуальным.
После выполнения метода-конструктора при вызове виртуальных методов их адреса определяются из ТВМ.
Деструктор.
Деструктор служит для очистки памяти и удаления динамически размещенного объекта. Обычно в деструкторе указываются завершающие действия с объектом. В заголовке деструктора служебное слово PROCEDURE заменяется на DESTRUCTOR Для одного и того же типа объекта можно определить несколько деструкторов. Деструкторы можно наследовать, они могут быть статическими или виртуальными.
V. Пример объектного типа
1. Постановка задачи: реализовать движение точки на экране.
2. Математическая модель: каждая точка на экране характеризуется координатами x, y и состоянием v –видима/невидима.
3. Объявление объектного типа точка:
POINT=object
X, Y: integer; {координаты}
V: boolean; {признак видимости: TRUE-видима; FALSE-невидима}
function GET_X: integer; { получение координаты Х }
function GET_Y: integer; { получение координаты Y }
function GET_V: boolean; { получение признака видимости }
procedure INIT(X0, Y0: integer); { задание координат }
procedure TURN_ON; { рисование точки – получение видимой точки }
procedure TURN_OFF; { стирание точки – получение невидимой точки}
procedure MOVE(XN, YN: integer); { перемещение точки }
4. Определение методов объекта POINT:
function POINT.GET_X; { получение координаты Х }
GET_X:=X;
function POINT.GET_Y; { получение координаты Y }
GET_Y:=Y;
function POINT.GET_V; { получение признака видимости }
GET_V:=V;
procedure POINT.INIT; { задание координат }
V:=false;
procedure POINT.TURN_ON; { получение видимой точки }
if not V then
PutPixel(X,Y,GetColor); { GetColor возвращает текущий цвет }
V:=true;
procedure POINT.TURN_OFF; { получение невидимой точки – стирание точки }
if V then
PutPixel(X,Y,GetBkColor); { GetBkColor возвращает фоновый цвет }
V:=false;
procedure POINT.MOVE; { перемещение точки }
F: boolean;
if F then
TURN_OFF;
if F then
TURN_ON;
Свойство наследования.
Кроме точки, можно задать другие объекты, описывающие геометрические фигуры, например, объект типа окружность , который определяется радиусом, координатой центра, признаком видимости , и с ним возможны такие же действия, как с точкой: получение видимой окружности; получение невидимой окружности; перемещение окружности (изменение координат центра). Чтобы этот объект привязать к координатной сетке экрана, необходимо определить объектный тип PLACE (место ), который будет иметь данные, общие для всех геометрических фигур, – координаты привязки объекта к экрану X и Y :
PLACE=object
X, Y: integer;
function GET_X: integer;
function GET_Y: integer;
Каждый новый объектный тип может строиться на основе ранее определенного типа, называемого родительским типом , от которого наследуются все поля и методы. Чтобы произошло наследование, в объявлении объектного типа после OBJECT в скобках указывается имя родительского типа.
Используя тип PLACE как родительский, можно определить объектный тип POINT таким образом:
POINT=object(PLACE)
V: boolean;
procedure INIT(X0, Y0: integer);
function GET_V:boolean;
procedure TURN_ON;
procedure TURN_OFF;
6. Свойство полиморфизма заключается в том, что в объявлении типа наследника могут быть методы, одноименные с именами методов родителя.
Объявление типа окружность c использованием в качестве родителя типа POINT будет следующим:
CIRCL=object(POINT)
R: integer;
procedure INIT(X0, Y0, R0: integer); { задание окружности }
procedure TURN_ON; { получение видимой окружности }
procedure TURN_OFF; { получение невидимой окружности }
procedure MOVE(XN, YN: integer); { перемещение окружности}
function GET_R: integer; { получение радиуса }
Определение методов с использованием наследования при полиморфизме:
procedure CIRCL.INIT;
X:=X0; Y:=Y0;
V:=false;
procedure CIRCL.TURN_ON;
V:=true;
CIRCLE(X,Y,R);
procedure CIRCL.TURN_OFF;
C: byte;
C:=GetColor;
SetColor(GetBkColor); { установка цвета рисования }
Circle(X,Y,R);
V:=false;
SetColor(C);
procedure CIRCL.MOVE;
F: boolean;
if F then
TURN_OFF;
if F then
TURN_ON;
function CIRCL.GET_R;
GET_R:=R;
В результате текст процедуры CIRCL.MOVE совпадает с текстом процедуры POINT.MOVE , но машинные коды у этих процедур будут разные; CIRCL.MOVE при своем исполнении обращается к адресам процедур CIRCL.TURN_ON и CIRCL.TURN_OFF , а процедура POINT.MOVE – к адресам процедур POINT.TURN_ON и POINT.TURN_OFF .
Раннее и позднее связывание методов.
Ранее рассмотренные статические методы компилятор размещает и разрешает все ссылки на них во время компиляции и компоновки (раннее связывание ).
Для примера, описанного ранее, в объекте CIRCL можно наследовать метод MOVE у объекта POINT. Методы TURN_ON и TURN_OFF должны быть объявлены виртуальными, чтобы с ними произошло позднее связывание.
Компилятор оставляет после компиляции неразрешенными ссылки к тем методам, которые объявлены виртуальными. Для описанного ранее примера в методе MOVE неразрешенными ссылками будут адреса методов TURN_ON, TURN_OFF , т. е. процедура MOVE будет не готова к исполнению после компиляции, т. к. ее машинный код полностью не определен.
Во время выполнения программы до исполнения виртуальных методов любого объекта должен выполниться метод-конструктор для данного экземпляра объекта, в который компилятор вставляет действия по связи неразрешенных ссылок с ТВМ.
Объявления объектных типов PLACE, POINT и CIRCL с использованием конструктора и виртуальных методов:
PLACE=object
X,Y: integer;
function GET_X: integer;
function GET_Y: integer;
POINT=object(PLACE)
V: boolean;
constructor INIT(X0, Y0: integer);
function GET_V: boolean;
procedure TURN_ON; virtual;
procedure TURN_OFF;virtual;
procedure MOVE(XN, YN: integer);
CIRCL=object(POINT)
R: integer;
constructor INIT(X0, Y0, R0: integer);
function GET_R: integer;
procedure TURN_ON; virtual;
procedure TURN_OFF; virtual;
Деструктор необходим только для динамически размещенных объектов, так как он определяют фактический размер объекта для корректного освобождения памяти от динамического объекта, причем эти действия выполняются деструктором по умолчанию в дополнение к действиям, описанным в теле деструктора.
данных . Целью полиморфизма, применительно к объектно-ориентированному программированию, является использование одного имени для задания общих для класса действий.
В языке Java объектные переменные являются полиморфными (polymorphic). Например:
class King { public static void main(String args) { King king = new King() ; king = new AerysTargaryen() ; king = new RobertBaratheon() ; } } class RobertBaratheon extends King { } class AerysTargaryen extends King { }
Переменная типа King может ссылаться как на объект типа King, так и на объект любого подкласса King.
Возьмем следующий пример:
class King { public void speech() { System .out .println ("I"m the King of the Andals!" ) ; } public void speech(String quotation) { System .out .println ("Wise man said: " + quotation) ; } public void speech(Boolean speakLoudly) { if (speakLoudly) System .out .println ("I"M THE KING OF THE ANDALS!!!11" ) ; else System .out .println ("i"m... the king..." ) ; } } class AerysTargaryen extends King { @Override public void speech() { System .out .println ("Burn them all..." ) ; } @Override public void speech(String quotation) { System .out .println (quotation+ " ... And now burn them all!" ) ; } } class Kingdom { public static void main(String args) { King king = new AerysTargaryen() ; king.speech ("Homo homini lupus est" ) ; } }
Что происходит, когда вызывается метод, принадлежащий объекту king ?
1. Компилятор проверяет объявленный тип объекта и имя метода, нумерует все методы с именем speech в классе AerusTargarien и все открытые методы speech в суперклассах AerusTargarien . Теперь компилятору известны возможные кандидаты при вызове метода.
2. Компилятор определяет типы передаваемых в метод аргументов. Если найден единственный метод, сигнатура которого совпадает с аргументами, происходит вызов. Этот процесс king.speech("Homo homini lupus est") компилятор выберет метод speech(String quotation) , а не speech() .
Если компилятор находит несколько методов с подходящими параметрами (или ни одного), выдается сообщение об ошибке.
Теперь компилятор знает имя и типы параметров метода,подлежащего вызову.
3. В случае, если вызываемый метод является private , static , final или конструктором, используется статическое связывание (early binding ). В остальных случаях метод, подлежащий вызову, определяется по фактическому типу объекта, через который происходит вызов. Т.е. во время выполнения программы используется динамическое связывание (late binding) .
4. Виртуальная машина заранее создает таблицу методов для каждого класса, в которой перечисляются сигнатуры всех методов и фактические методы, подлежащие вызову.
Таблица методов для класса King выглядит так:
- speech() - King . speech()
- speech(String quotation) - King . speech(String quotation )
- King . speech(Boolean speakLoudly )
- speech() - AerysTargaryen . speech()
- speech(String quotation) - AerysTargaryen . speech(String quotation )
- speech(Boolean speakLoudly) - King . speech(Boolean speakLoudly )
При вызове king. speech() :
- Определяется фактический тип переменной king . В данном случае это AerysTargaryen .
- Виртуальная машина определяет класс, к которому принадлежит метод speech()
- Происходит вызов метода.
А что произойдет, если вызвать в конструкторе динамически связываемый метод конструируемого объекта? Например:
class King { King() { System .out .println ("Call King constructor" ) ; speech() ; //polymorphic method overriden in AerysTargaryen } public void speech() { System .out .println ("I"m the King of the Andals!" ) ; } } class AerysTargaryen extends King { private String victimName; AerysTargaryen() { System .out .println ("Call Aerys Targaryen constructor" ) ; victimName = "Lyanna Stark" ; speech() ; } @Override public void speech() { System .out .println ("Burn " + victimName + "!" ) ; } } class Kingdom { public static void main(String args) { King king = new AerysTargaryen() ; } } Результат:
Call King constructor Burn null! Call Aerys Targaryen constructor Burn Lyanna Stark !
Конструктор базового класса всегда вызывается в процессе конструирования производного класса. Вызов автоматически проходит вверх по цепочке наследования, так что в конечном итоге вызываются конструкторы всех базовых классов по всей цепочке наследования.
Это значит, что при вызове конструктора new AerysTargaryen() будут вызваны:
- new Object()
- new King()
- new AerysTargaryen()
Однако динамически связываемый вызов может перейти во «внешнюю» часть иерархии, то есть к производным классам. Если он вызовет метод производного класса в конструкторе, это может привести к манипуляциям с неинициализированными данными, что мы и видим в результате работы данного примера.
Результат работы программы обусловлен выполнение алгоритма иницализации объекта:
- Память, выделенная под новый объект, заполняется двоичными нулями.
- Конструкторы базовых классов вызываются в описанном ранее порядке. В этот момент вызывается переопределенный метод speech() (да, перед вызовом конструктора класса AerysTargaryen ), где обнаруживается, что переменная victimName равна null из-за первого этапа.
- Вызываются инициализаторы членов класса в порядке их определения.
- Исполняется тело конструктора производного класса.
- выполняйте в конструкторе лишь самые необходимые и простые действия по инициализации объекта
- по возможности избегайте вызова методов, не определенных как private или final (что в данном контексте одно и то же).
Использованы материалы:
- Eckel B. - Thinking in Java , 4th Edition - Chapter 8
- Cay S. Horstmann, Gary Cornell - Core Java 1 - Chapter 5
- Wikipedia
Связывание в языке C++
Двумя основными целями при разработке языка программирования С++ были эффективное использование памяти и скорость выполнения. Он был задуман как усовершенствование языка С, в частности, для объектно-ориентированных приложений. Основной принцип С++: никакое свойство языка не должно приводить к возникновению дополнительных издержек (как по памяти, так и по скорости), если данное свойство программистом не используется. Например, если вся объектная ориентированность С++ игнорируется, то оставшаяся часть должна работать так же быстро, как и традиционный С. Поэтому неудивительно что большинство методов в С++ связываются статически (во время компиляции), а не динамически (во время выполнения).
Связывание методов в этом языке является довольно сложным. Для обычных переменных (не указателей или ссылок) оно осуществляется статически. Но когда объекты обозначаются с помощью указателей или ссылок, используется динамическое связывание. В последнем случае решение о выборе метода статического или динамического типа диктуется тем, описан ли соответствующий метод с помощью ключевого слова virtual. Если он объявлен именно так, то метод поиска сообщения базируется на динамическом классе, если нет на статическом. Даже в тех случаях, когда используется динамическое связывание, законность любого запроса определяется компилятором на основе статического класса получателя.
Рассмотрим, например, следующее описание классов и глобальных переменных: class Mammal
printf («cant speak»);
class Dog: public Mammal
printf («wouf wouf»);
printf («wouf wouf, as well»);
Mammal *fido = new Dog;
Выражение fred.speak() печатает «cant speak», однако вызов fido->speak() также напечатает «cant speak», поскольку соответствующий метод в классе Mammal не объявлен как виртуальный. Выражение fido->bark() не допускается компилятором, даже если динамический тип для fido класс Dog. Тем не менее статический тип переменной всего лишь класс Mammal.
Если мы добавим слово virtual:
virtual void speak()
printf («cant speak»);
то получим на выходе для выражения fido->speak() ожидаемый результат.
Относительно недавнее изменение в языке С++ добавление средств для распознавания динамического класса объекта. Они образуют систему RTTI (Run-Time Type Identification идентификация типа во время выполнения).
В системе RTTI каждый класс имеет связанную с ним структуру типа typeinfo, которая кодирует различную информацию о классе. Поле данных name одно из полей данных этой структуры содержит имя класса в виде текстовой строки. Функция typeid может использоваться для анализа информации о типе данных. Следовательно, следующая ниже команда будет печатать строку «Dog» динамический тип данных для fido. В этом примере необходимо разыменовывать переменную-указатель fido, чтобы аргумент был значением, на которое ссылается указатель, а не самим указателем:
cout << «fido is a» << typeid(*fido).name() << endl;
Можно также спросить, используя функцию-член before, соответствует ли одна структура с информацией о типе данных подклассу класса, соотносящегося с другой структурой. Например, следующие два оператора выдают true и false:
if (typeid(*fido).before (typeid(fred)))…
if (typeid(fred).before (typeid(lassie)))…
До появления системы RTTI стандартный программистский трюк состоял в том, чтобы явным образом закодировать в иерархии класса методы быть экземпляром. Например, для проверки значения переменных типа Animal на принадлежность к типу Cat или к типу Dog можно было бы определить следующую систему методов:
virtual int isaDog()
virtual int isaCat()
class Dog: public Mammal
virtual int isaDog()
class Cat: public Mammal
virtual int isaCat()
Теперь для определения того, является ли текущим значением переменной fido величина типа Dog, можно использовать команду fido->isaDog(). Если возвращается ненулевое значение, то можно привести тип переменной к нужному типу данных.
Возвращая указатель, а не целое число, мы объединяем проверку на принадлежность к подклассу и приведение типа. Это аналогично другой части системы RTTI, называемой dynamic_cast, которую мы вкратце опишем. Если некая функция в классе Mammal возвращает указатель на Dog, то класс Dog должен быть предварительно описан. Результатом присваивания является либо нулевой указатель, либо правильная ссылка на класс Dog. Итак, проверка результата все еще должна осуществляться, но мы исключаем необходимость приведения типа. Это показано в следующем примере:
class Dog; // предварительное описание
virtual Dog* isaDog()
virtual Cat* isaCat()
class Dog: public Mammal
virtual Dog* isaDog()
class Cat: public Mammal
virtual Cat* isaCat()
Оператор lassie = fido->isaDog(); теперь выполним всегда. В результате переменная lassie получает ненулевое значение, только если fido имеет динамический класс Dog. Если fido не принадлежит Dog, то переменной lassie будет присвоен нулевой указатель.
lassie = fido->isaDog();
… // fido и в самом деле относится к типу Dog
… // присваивание не сработало
… // fido не принадлежит к типу Dog
Хотя программист и может использовать этот метод для обращения полиморфизма, недостаток такого способа состоит в том, что требуется добавление методов как в родительский, так и в дочерний классы. Если из одного общего родительского класса проистекает много дочерних, метод становится громоздким. Если изменения в родительском классе не допускаются, такая техника вообще невозможна.
Поскольку подобные проблемы встречаются часто, было найдено их общее решение. Функция шаблона dynamic_cast берет тип в качестве аргумента шаблона и, в точности как функция, определенная выше, возвращает либо значение аргумента (если приведение типа законно), либо нулевое значение (если приведение типа неразрешено). Присваивание, эквивалентное сделанному в предыдущем примере, может быть записано так:
// конвертировать только в том случае, если fido является собакой
lassie = dynamic_cast < Dog* > (fido);
// затем проверить, выполнено ли приведение
В язык C++ были добавлены еще три типа приведения (static_cast, const_cast и reinterpret_cast), но они используются в особых случаях и поэтому здесь не описываются. Программистам рекомендуется применять их как более безопасные средства вместо прежнего механизма приведения типов.
2. Проектная часть
Последнее обновление: 04.02.2019
Ранее было рассмотрена два способа изменения функциональности методов, унаследованных от базового класса - сокрытие и переопределение. В чем разница между двумя этими способами?
Переопределение
Возьмем пример с переопределением методов:
Class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public virtual void Display() { Console.WriteLine($"{FirstName} {LastName}"); } } class Employee: Person { public string Company { get; set; } public Employee(string firstName, string lastName, string company) : base(firstName, lastName) { Company = company; } public override void Display() { Console.WriteLine($"{FirstName} {LastName} работает в {Company}"); } }
Также создадим объект Employee и передадим его переменной типа Person:
Person tom = new Employee("Tom", "Smith", "Microsoft"); tom.Display(); // Tom Smith работает в Microsoft
Теперь мы получаем иной результат, нежели при сокрытии. А при вызове tom.Display() выполняется реализация метода Display из класса Employee.
Для работы с виртуальными методами компилятор формирует таблицу виртуальных методов (Virtual Method Table или VMT). В нее записывается адреса виртуальных методов. Для каждого класса создается своя таблица.
Когда создается объект класса, то компилятор передает в конструктор объекта специальный код, который связывает объект и таблицу VMT.
А при вызове виртуального метода из объекта берется адрес его таблицы VMT. Затем из VMT извлекается адрес метода и ему передается управление. То есть процесс выбора реализации метода производится во время выполнения программы. Собственно так и выполняется виртуальный метод. Следует учитывать, что так как среде выполнения вначале необходимо получить из таблицы VMT адрес нужного метода, то это немного замедляет выполнение программы.
Сокрытие
Теперь возьмем те же классы Person и Employee, но вместо переопределения используем сокрытие:
Class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public void Display() { Console.WriteLine($"{FirstName} {LastName}"); } } class Employee: Person { public string Company { get; set; } public Employee(string firstName, string lastName, string company) : base(firstName, lastName) { Company = company; } public new void Display() { Console.WriteLine($"{FirstName} {LastName} работает в {Company}"); } }
И посмотрим, что будет в следующем случае:
Person tom = new Employee("Tom", "Smith", "Microsoft"); tom.Display(); // Tom Smith
Переменная tom представляет тип Person, но хранит ссылку на объект Employee. Однако при вызове метода Display будет выполняться та версия метода, которая определена именно в классе Person, а не в классе Employee. Почему? Класс Employee никак не переопределяет метод Display, унаследованный от базового класса, а фактически определяет новый метод. Поэтому при вызове tom.Display() вызывается метод Display из класса Person.