Введение в ООП с примерами на C#. Часть третья. Практические аспекты использования полиморфизма

Рассказывает Akhil Mittal


Введение

Раньше в этой серии мы говорили о полиморфизме и наследовании.

В этой статье мы опять будем говорить о полиморфизме, но в этот раз сосредоточимся именно на практических нюансах, а не на теории. Если вы овладеете технологией, описанной в этой статье, то считайте, что изучили 50% ООП.

Ключевые слова New и Override в C#

Для начала создадим новое консольное приложение и два класса в нём:

Мы видим, что эти классы содержат три метода с попарно одинаковыми именами. Теперь выполним следующий код из Program.cs:

Жмём F5, т.е. выполняем код, и что мы видим?

ClassA AAA
ClassA BBB
ClassA CCC

ClassB AAA
ClassB BBB
ClassB CCC

ClassA AAA
ClassA BBB
ClassA CCC

Но кроме вывода, мы получили ещё и три предупреждения от компилятора:

‘InheritanceAndPolymorphism.ClassB.AAA()’ hides inherited member
‘InheritanceAndPolymorphism.ClassA.AAA()’. Use the new keyword if hiding was intended.

‘InheritanceAndPolymorphism.ClassB.BBB()’ hides inherited member
‘InheritanceAndPolymorphism.ClassA.BBB()’. Use the new keyword if hiding was intended.

‘InheritanceAndPolymorphism.ClassB.CCC()’ hides inherited member
‘InheritanceAndPolymorphism.ClassA.CCC()’. Use the new keyword if hiding was intended.

Что нужно запомнить: мы можем записать в переменную класса-родителя объект наследника, но не наоборот.

ClassA — родитель ClassB. То есть ClassB содержит то, что находится в ClassA и ещё что-то своё. В этом причина правила, которое мы записали выше: класс-родитель не содержит описания всех необходимых полей и методов класса-наследника, поэтому мы не можем использовать ClassA как ClassB.

Теперь посмотрим, что у нас происходит в коде. С x и y всё понятно: они объявлены и инициализированны одним и тем же типом. Рассмотрим подробнее z. Эта переменная типа ClassB, а её значение — объект типа ClassA, хотя в данном контексте нет никакой разницы, какого типа её значение, вывод всегда будет аналогичен выводу от y. Выбор метода по типу ссылки, а не по типу объекта — это стандартное поведение, когда явно не указан приоритет методов, о чём свидетельствуют warning’и. Как же описать требуемое поведение? Здесь нам как раз помогут ключевые слова new и override.

Давайте проведём эксперимент

Добавим к двум методам из ClassB ключевые слова new и override следующим образом:

Если мы сейчас выполним Program.cs, то на выходе получим:

Error: ‘InheritanceAndPolymorphism.ClassB.AAA()’: cannot override inherited member
‘InheritanceAndPolymorphism.ClassA.AAA()’ because it is not marked virtual, abstract, or override

Ошибка возникает из-за того, что поля родителя не помечены ключевым словом virtual. Этот модификатор обозначает, что мы имеем право вызывать метод из дочернего класса или перезаписывать его. Добавим virtual ко всем методам ClassA:

И снова запустим Program.cs:

ClassB AAA
ClassB BBB
ClassB CCC

ClassA AAA
ClassA BBB
ClassA CCC

ClassB AAA
ClassA BBB
ClassA CCC

Очевидно, что метод дочернего класса вызвался только там, где стоял модификатор override. В свзяи с чем делаем вывод: override значит, что помеченный метод — новая версия родительского и должен использоваться вместо него. И, напротив, new обозначает, что метод, хоть и случайно имеет такое же имя, является абсолютно по сути абсолютно другим, а значит, в нашем примере должен выполняться метод родительского класса. Если мы не пишем никакого модификатора, мы подразумеваем именно new.
Разберём подробнее логику C#. Когда вызывается метод какого-то объекта по ссылке, то в первую очередь он смотрит на тип ссылки. Если в этом классе обнаружен модификатор virtual, он начинает искать среди дочерних классов тип объекта, и, если встречает new, запускает последний override метод, который встретил (либо метод типа ссылки). Возможно, это не слишком понятно, обратимся к более сложному примеру.

Эксперимент с тремя классами

Результатом такого эксперимента станет:

ClassA AAA
ClassA BBB
ClassB CCC

ClassA AAA
ClassA BBB
ClassB CCC

ClassC AAA
ClassB BBB
ClassB CCC

В первом случае мы имеем дело с типом ссылки ClassA и типом объекта ClassB. Компилятор действует вполне очевидно:

Во втором случае тип объекта у нас уже ClassC. Поскольку он наследуется от ClassA не напрямую, а через ClassB, наша диаграмма будет уже несколько сложнее.

В третьем случае мы имеем дело снова с двумя классами, ClassA мы просто игнорируем. Если учесть это, то вывод будет очевиден, но всё же вот схема:

Важным замечанием будет, что следующий код:

Выдаст ошибку:

Error: ‘InheritanceAndPolymorphism.C.X()’: cannot override inherited member
‘InheritanceAndPolymorphism.B.X()’ because it is not marked virtual, abstract, or override

Ведь из-за того, что в B метод помечен как new, он не наследует свойство virtual, а значит не может быть перезаписан с помощью override в C. Правильным вариантом было бы добавить к описанию метода в B ключевое слово virtual или изменить в C override на new, в зависимости от требуемого поведения.

Ключевое слово base

Теперь, когда мы разобрались с самой сложной частью, стоит напомнить про возможность вызова методов родительского класса из дочернего. Простой пример:

Даст нам вывод:

ClassA XXX
ClassB XXX

ClassA XXX
ClassB XXX
ClassC XXX

В первом случае выполняется метод ClassB, который через base вызывает метод из ClassA. Во втором — XXX() из ClassC, который обращается к ClassB, а тот, в свою очередь, к ClassA.

Немного рекурсии

В этом примере вызов ClassB.XXX() всегда будет приводить к созданию нового объекта типа ClassB в ссылке ClassA. Очевидно, что по такой ссылке снова будет вызван ClassB.XXX() и т.д. В данном случае выводом будет ошибка:

Error: {Cannot evaluate expression because the current thread is in a stack overflow state.}

В заключение

Подведём итоги:

  • В C# мы можем записать в переменную класса-родителя объект наследника, но не наоборот;
  • Модификатор override используется, чтобы указать на то, что должен вызваться метод именно дочернего класса;
  • Чтобы использовать модификаторы override и new, метод родительского класса должен быть помечен ключевым словом virtual;
  • Когда вызывается метод какого-то объекта по ссылке, то С# в первую очередь смотрит на тип ссылки. Если в этом классе обнаружен модификатор virtual, он начинает искать среди дочерних классов тип объекта, и, если встречает new, зупускает последний override метод, который встретил (либо метод типа ссылки).

Источник: «Diving in OOP (Day 3): Polymorphism and Inheritance (Dynamic Binding/Run Time Polymorphism)»