Программирование на C# (Си Шарп) - ООП в .NET - Полиморфизм и виртуальные члены

Программирование на C# - ООП C#

ООП С Sharp - Полиморфизм и виртуальные члены

Вернемся к примеру с Mortimer Phones. Ранее мы уже видели такую строку кода:

Nevermore60Customer arabel = new Nevermore60Customer();

Фактически, можно создать экземпляр объекта Nevermore60Customer и так:

Customer arabel = new Nevermore60Customer();

Поскольку Nevermore60Customer унаследован от Customer, совершенно законно, если ссылка типа Customer будет установлена либо на Nevermore60Customer, либо на Customer, либо на экземпляр любого другого класса, непосредственно унаследованного от Customer. Обратите внимание, что все, что здесь меняется — это объявление ссылочной переменной.

В действительности операцией new создается объект Nevermore60Customer. Если же, например, вы попытаетесь вызвать метод GetType() с этим объектом, то он сообщит вам, что объект имеет тип Nevermore60Customer.

Возможность базовой ссылки указывать на классы-наследники может показаться лишь синтаксическим удобством, но на самом деле она очень важна для простого использования объектов классов-наследников, что является существенным средством для любого языка, который претендует на поддержку ООП. Вы поймете, почему это так, если задумаетесь о том, как реальные компании-операторы сотовой связи хотят хранить разнообразные классы-наследники Customer. В данном примере у нас только два абонента, поэтому несложно определить для них отдельные переменные. Однако в реальном мире существуют сотни тысяч абонентов, и вы захотите каким-то образом читать их из базы данных в массив, затем обрабатывать, используя этот массив в коде, выглядящем примерно так:

 
Customer[] customers = new Customer[NCustomers];
// делать что-то для инициализации абонентов
foreach (Customer nextCustomer in customers)
{
Console.WriteLine("{0,-20} должен ${1:F2}", nextCustomer.Name,
nextCustomer.Balance);
} 

Если вы используете массив ссылок Customer, то каждый его элемент может указывать на абонента любого типа, независимо от того, какой класс-наследник Customer применяется для его представления. Однако если бы переменные не могли хранить ссылки на унаследованные типы, то пришлось бы иметь множество массивов — массив Customer, массив Nevermore60Customer другие массивы для каждого типа класса. А так вы можете смешивать различные типы классов в одном массиве, и это не создаст для компилятора никаких проблем. Предположим, есть следующий фрагмент кода:

 
Customer aCustomer;
// Инициализация aCustomer определенным тарифом
aCustomer.RecordCall(TypeOfCall.CallToLandline, 20); 

Что может видеть компилятор — так это ссылку Customer, и вы должны вызывать с ним метод RecordCall(). Проблема в том, что aCustomer может ссылаться на экземпляр Customer, а может и на экземпляр Nevermore60Customer или же на экземпляр некоторого другого класса, унаследованного от Customer. Каждый из этих классов может иметь собственную реализацию RecordCall(). Как же компилятор определит, какой именно метод должен быть вызван?

Существует два ответа на этот вопрос, в зависимости от того, объявлен ли этот метод в базовом классе как virtual, а в наследнике как override:

  • Если методы не объявлены, соответственно, как virtual и override, то компилятор просто использует тип, объявленный для данной ссылки. В этом случае, поскольку aCustomer имеет тип Customer, вызывается метод Customer.RecordCall(), независимо от того, на что именно указывает aCustomer.
  • Если методы объявлены как virtual и override соответственно, то компилятор сгенерирует код, проверяющий, на что именно указывает ссылка aCustomer, во время выполнения. Затем он идентифицирует, к какому классу принадлежит данный экземпляр и вызовет соответствующий вариант RecordCall(). Это определение необходимой перегрузки понадобится при каждом выполнении оператора. Например, если вызов virtual метода произойдет внутри цикла foreach, выполняющегося 100 раз, то на каждой итерации цикла ссылка может указывать на разные экземпляры, а потому и на объекты разных классов.

В большинстве случаев вам потребуется именно второй вариант поведения. Если, например, у вас есть ссылка на Nevermore60Customer, то весьма маловероятно, что вы захотите вызвать любое переопределение любого метода, отличного от того, что относится к экземпляру Nevermore60Customer. На самом деле скорее можно удивляться тому, зачем вообще может понадобиться первый, невиртуальный подход, поскольку он в большинстве случаев означает вызов “неправильного” переопределения метода. Почему бы вообще не сделать все методы по умолчанию virtual, чтобы это стало нормальным поведением? Кстати, именно такой подход принят в Java, где все методы автоматически являются virtual. Есть три веских причины, чтобы не делать этого в C#: \

  • Производительность. Когда вызывается виртуальный метод, исполняющая система должна определить, какой именно вариант метода нужно вызвать. Для невиртуальных функций эта информация доступна во время компиляции (компилятор может идентифицировать подходящее переопределение по объявленному типу ссылки). Это значит, что для невиртуальных функций компилятор может выполнить оптимизацию, такую как встраивание кода, чтобы повысить производительность. Встраивание виртуальных методов невозможно, от чего страдает производительность. Другой (менее важный) фактор состоит в том, что определение самого метода очень незначительно влияет на производительность. Это влияние ограничено не более ем дополнительным поиском адреса в таблице виртуальных функций (называемой vtable), и потому не очень существенно в большинстве случаев, но может оказаться таковым в очень плотных и часто исполняемых циклах.
  • Дизайн. При проектировании класса может оказаться, что должны существовать методы, которые не должны переопределяться. И это случается достаточно часто, особенно с методами, которые должны использоваться внутри класса другими его методами, либо чья реализация отражает внутренний дизайн класса. Когда вы проектируете класс, то выбираете, какие средства реализации следует сделать общедоступными, защищенными или приватными. Маловероятно, что вы захотите, чтобы методы, которые в первую очередь касаются внутренних операций класса, были переопределяемыми, а потому не станете объявлять их виртуальными.
  • Контроль версий. Виртуальные методы могут служить причиной некоторых проблем, связанных с реализацией новых версий базовых классов.

Возможность использования переменной в качестве ссылки на объекты разных типов и автоматического вызова соответствующей версии метода ссылаемого объекта более известна формально как полиморфизм. Однако вы должны отметить, что для того, чтобы использовался полиморфизм, вызываемый метод должен присутствовать как в базовом классе, так и в классе-наследнике. Например, предположим, что вы добавили в класс Nevermore60Customer какой-то другой метод, например, свойство по имени HighCostMinutesLeft, чтобы обеспечить пользователю доступ к этому кусочку информации. Тогда следующий фрагмент кода является совершенно законным:

 
Nevermore60Customer mrLeggit = new Nevermore60Customer();
// обработка
int minutesLeft = mrLeggit.HighCostMinutesLeft; 

А вот следующий код не будет законным, поскольку свойство HighCostMinutesLeft отсутствует в базовом классе Customer:

 
Customer mrLeggit = new Nevermore60Customer();
// обработка
int minutesLeft = mrLeggit.HighCostMinutesLeft; 

Есть еще несколько моментов, связанных с виртуальными членами, о которых не следует забывать:

  • Переопределенными или скрытыми могут быть не только методы. То же самое можно делать с любыми другими членами класса, имеющими реализацию, включая свойства.
  • Поля не могут быть определены как virtual или override. Однако можно скрыть базовую версию поля, объявив в классе-наследнике другое поле под тем же именем. В этом случае, если нужно обратиться к базовой версии из классанаследника, следует использовать синтаксис base.<field_name>. На самом деле вам не придется делать и этого, потому что лучше объявлять все поля как private.
  • Статические методы и тому подобное не могут быть объявлены как virtual, но они могут быть скрыты таким же образом, как методы экземпляров и все прочие методы. Нет смысла объявлять член static как virtual, потому что virtual означает, что компилятор ищет экземпляр класса, когда вызывает его член, а статические члены не ассоциированы ни с одним экземпляром класса.
  • То, что метод объявлен как virtual, еще не означает, что он должен быть переопределен. Вообще, если компилятор встречает вызов виртуального метода, то сначала он ищет определение этого метода в рассматриваемом классе. Если метод не определен и не переопределен в этом классе, то вызывается версия метода из базового класса. Если же метод не определен и там, то просматривается следующий базовый класс и так далее, так что выполняется метод, ближайший в иерархии к рассматриваемому классу (отметим, что этот процесс происходит во время компиляции, когда компилятор генерирует vtable для каждого класса. Это не оказывает никакого влияния во время выполнения).

 

Добавить комментарий


Защитный код
Обновить