Полиморфизм часто называется третьим столпом объектно-ориентированного программирования после инкапсуляции и наследования. Полиморфизм — слово греческого происхождения, означающее «многообразие форм» и имеющее несколько аспектов.
- Во время выполнения объекты производного класса могут обрабатываться как объекты базового класса в таких местах, как параметры метода и коллекции или массивы. Когда возникает полиморфизм, объявленный тип объекта перестает соответствовать своему типу во время выполнения.
- Базовые классы могут определять и реализовывать виртуальныеметоды, а производные классы — переопределять их, т. е. предоставлять свое собственное определение и реализацию. Во время выполнения, когда клиент вызывает метод, CLR выполняет поиск типа объекта во время выполнения и вызывает перезапись виртуального метода. В исходном коде можно вызвать метод в базовом классе и обеспечить выполнение версии метода, относящейся к производному классу.
Виртуальные методы позволяют работать с группами связанных объектов универсальным способом. Представим, например, приложение, позволяющее пользователю создавать различные виды фигур на поверхности для рисования. Во время компиляции вы не знаете, какие конкретные типы фигур будет создавать пользователь. При этом приложению необходимо отслеживать все различные типы создаваемых фигур и обновлять их в ответ на движения мыши. Для решения этой проблемы можно использовать полиморфизм, выполнив два основных действия.
C# — Полиморфизм. Уроки для маленьких и тупых #13.
- Создать иерархию классов, в которой каждый отдельный класс фигур является производным из общего базового класса.
- Применить виртуальный метод для вызова соответствующего метода на любой производный класс через единый вызов в метод базового класса.
Для начала создайте базовый класс с именем Shape и производные классы, например Rectangle , Circle и Triangle . Присвойте классу Shape виртуальный метод с именем Draw и переопределите его в каждом производном классе для рисования конкретной фигуры, которую этот класс представляет. Создайте объект List и добавьте в него Circle , Triangle и Rectangle .
public class Shape < // A few example members public int X < get; private set; >public int Y < get; private set; >public int Height < get; set; >public int Width < get; set; >// Virtual method public virtual void Draw() < Console.WriteLine(«Performing base class drawing tasks»); >> public class Circle : Shape < public override void Draw() < // Code to draw a circle. Console.WriteLine(«Drawing a circle»); base.Draw(); >> public class Rectangle : Shape < public override void Draw() < // Code to draw a rectangle. Console.WriteLine(«Drawing a rectangle»); base.Draw(); >> public class Triangle : Shape < public override void Draw() < // Code to draw a triangle.
Console.WriteLine(«Drawing a triangle»); base.Draw(); >>
Ё*кий полиморфизм
Для обновления поверхности рисования используйте цикл foreach, чтобы выполнить итерацию списка и вызвать метод Draw на каждом объекте Shape в списке. Несмотря на то, что каждый объект в списке имеет объявленный тип Shape , будет вызван тип времени выполнения (переопределенная версия метода в каждом производном классе).
// Polymorphism at work #1: a Rectangle, Triangle and Circle // can all be used wherever a Shape is expected. No cast is // required because an implicit conversion exists from a derived // class to its base class. var shapes = new List < new Rectangle(), new Triangle(), new Circle() >; // Polymorphism at work #2: the virtual method Draw is // invoked on each of the derived classes, not the base class. foreach (var shape in shapes) < shape.Draw(); >/* Output: Drawing a rectangle Performing base class drawing tasks Drawing a triangle Performing base class drawing tasks Drawing a circle Performing base class drawing tasks */
В C# каждый тип является полиморфным, так как все типы, включая пользовательские, наследуют Object.
Обзор полиморфизма
Виртуальные члены
Если производный класс наследуется от базового класса, он включает все члены базового класса. Все поведение, объявленное в базовом классе, является частью производного класса. Это позволяет обрабатывать объекты производного класса как объекты базового класса. Модификаторы доступа ( public , и private т. д.) определяют, protected доступны ли эти члены из реализации производного класса. Виртуальные методы предоставляют конструктору различные варианты поведения производного класса:
- Производный класс может переопределять виртуальные члены в базовом классе, определяя новое поведение.
- Производный класс может наследовать ближайший метод базового класса, не переопределяя его, сохраняя существующее поведение, но позволяя дальнейшим производным классам переопределять метод.
- Производный класс может определить новую, невиртуальную реализацию тех членов, которые скрывают реализации базового класса.
Производный класс может переопределить член базового класса, только если последний будет объявлен виртуальным или абстрактным. Производный член должен использовать ключевое слово override, указывающее, что метод предназначен для участия в виртуальном вызове. Примером является следующий код:
public class BaseClass < public virtual void DoWork() < >public virtual int WorkProperty < get < return 0; >> > public class DerivedClass : BaseClass < public override void DoWork() < >public override int WorkProperty < get < return 0; >> >
Поля не могут быть виртуальными; Виртуальными могут быть только методы, свойства, события и индексаторы. Когда производный класс переопределяет виртуальный член, он вызывается даже в то случае, если доступ к экземпляру этого класса осуществляется в качестве экземпляра базового класса. Примером является следующий код:
DerivedClass B = new DerivedClass(); B.DoWork(); // Calls the new method. BaseClass A = B; A.DoWork(); // Also calls the new method.
Виртуальные методы и свойства позволяют производным классам расширять базовый класс без необходимости использовать реализацию базового класса метода. Дополнительные сведения см. в разделе Управление версиями с помощью ключевых слов Override и New.
Еще одну возможность определения метода или набора методов, реализация которых оставлена производным классам, дает интерфейс.
Сокрытие членов базового класса новыми членами
Если вы хотите, чтобы производный класс имел член с тем же именем, что и член в базовом классе, можно использовать ключевое слово new, чтобы скрыть член базового класса. Ключевое слово new вставляется перед типом возвращаемого значения замещаемого члена класса. Примером является следующий код:
public class BaseClass < public void DoWork() < WorkField++; >public int WorkField; public int WorkProperty < get < return 0; >> > public class DerivedClass : BaseClass < public new void DoWork() < WorkField++; >public new int WorkField; public new int WorkProperty < get < return 0; >> >
Доступ к скрытым членам базового класса можно осуществлять из клиентского кода приведением экземпляра производного класса к экземпляру базового класса. Пример:
DerivedClass B = new DerivedClass(); B.DoWork(); // Calls the new method. BaseClass A = (BaseClass)B; A.DoWork(); // Calls the old method.
Защита виртуальных членов от переопределения производными классами
Виртуальные члены остаются виртуальными независимо от количества классов, объявленных между виртуальным членом и классом, который объявил его изначально.
Если класс A объявляет виртуальный член, класс B является производным от класса A , а класс C — от класса B , то класс C наследует виртуальный член и может переопределить его независимо от того, объявляет ли класс B переопределение этого члена. Примером является следующий код:
public class A < public virtual void DoWork() < >> public class B : A < public override void DoWork() < >>
Производный класс может остановить виртуальное наследование, объявив переопределение как запечатанное. Для остановки наследования в объявление члена класса нужно вставить ключевое слово sealed перед ключевым словом override . Примером является следующий код:
public class C : B < public sealed override void DoWork() < >>
В предыдущем примере метод DoWork более не является виртуальным ни для одного класса, производного от класса C . Он по-прежнему является виртуальным для экземпляров класса C , даже если они приводятся к типу B или типу A . Запечатанные методы можно заменить производными классами с помощью ключевого слова new , как показано в следующем примере:
public class D : C < public new void DoWork() < >>
В этом случае, если DoWork вызывается для D с помощью переменной типа D , вызывается новый DoWork . Если переменная типа C , B или A используется для доступа к экземпляру D , вызов DoWork будет выполняться по правилам виртуального наследования и направлять эти вызовы в реализацию DoWork в классе C .
Доступ к виртуальным членам базового класса из производных классов
Производный класс, который заменил или переопределил метод или свойство, может получить доступ к методу или свойству на базовом классе с помощью ключевого слова base . Примером является следующий код:
public class Base < public virtual void DoWork() > public class Derived : Base < public override void DoWork() < //Perform Derived’s work here //. // Call DoWork on base class base.DoWork(); >>
Дополнительные сведения см. в разделе base.
Рекомендуется, чтобы виртуальные члены использовали base для вызова реализации базового класса этого члена в их собственной реализации. Разрешение поведения базового класса позволяет производному классу концентрироваться на реализации поведения, характерного для производного класса. Если реализация базового класса не вызывается, производный класс сопоставляет свое поведение с поведением базового класса по своему усмотрению.
Источник: learn.microsoft.com
C # — Полиморфизм
Слово полиморфизм означает наличие многих форм. В парадигме объектно-ориентированного программирования полиморфизм часто выражается как «один интерфейс, несколько функций».
Полиморфизм может быть статическим или динамическим. В статическом полиморфизме ответ на функцию определяется во время компиляции. В динамическом полиморфизме он решается во время выполнения.
Статический полиморфизм
Механизм связывания функции с объектом во время компиляции называется ранним связыванием. Он также называется статической привязкой. C # предоставляет два метода для реализации статического полиморфизма. Они —
- Перегрузка функций
- Перегрузка оператора
NEWOBJ.ru → Введение в ООП с примерами на C# →
Переменная shape типа Shape в первой итерации ( i == 0 ) обозначает объект типа Triangle , во второй итерации ( i == 1 ) объект типа Circle , в третьей ( i == 2 ) – Polygon . Мы подробно рассматривали эту ситуацию в предыдущих главах. В общем случае метод Scale может быть: 1) не виртуальным методом базового класса Shape ; 2) виртуальным или абстрактным методом, переопределенным в производном классе; 3) методом интерфейса Shape (если Shape – интерфейс), реализуемом в соответствующем классе, реализующем этот интерфейс ( Triangle , Circle или Polygon ). Обратим внимание, что конкретная реализация метода, которую нужно вызывать во втором и третьем случае, определяется на этапе выполнения программы и не может быть определена на этапе компиляции. Эта возможность объектно-ориентированных языков программирования – записи кода с использованием переменных базовых типов (классов или интерфейсов), откладывая до момента выполнения кода определение того, какую именно реализацию метода нужно выполнить, и называется полиморфизмом. Прежде всего полиморфизм позволяет существенно повысить уровень абстракции: мы оперируем ровно с тем минимально детализированным представлением объекта (методом Shape.Scale ), которое нам нужно в данный момент, не отвлекаясь на особенности производных классов.
Полиморфизм (polymorphism) – возможность одной и той же переменной в различные моменты выполнения программы обозначать объекты различных типов (классов), относящихся к одному базовому типу (классу или интерфейсу).
Сам термин «полиморфизм» составлен из греческих слов πολύς, много, и μορφή, форма, и позаимствован из естественных наук.
По большому счету, полиморфизм – основной выигрыш от использования иерархических типов данных. Практическая значимость полиморфизма уже подробно анализировалась нами в предыдущих главах при рассмотрении механизмов наследования, приведения переменных производных классов к базовым, виртуальных и абстрактных методов, интерфейсов. Главное при этом – возможность писать более лаконичный и обобщенный код, опираясь на базовые типы, и, тем самым, повышать уровень абстракции и снижать сложность программы. Читатель может самостоятельно вернуться к материалу глав 3.1 – 3.4 и показать, где именно шла речь о полиморфизме и какие именно мы получали практические преимущества от его использования.
Остановимся особо на вопросе о том, что на этапе компиляции неизвестно, к какому именно конкретному типу будет относиться объект и, соответственно, какие именно реализации вызываемых методов следует использовать. Компилятор не может связать вызов метода с реализацией этого метода. Термин «связывание» здесь обозначает сопоставление имени метода в коде (в примере: shape.Scale ) с конкретной реализацией этого метода (адресом кода метода в памяти). Если связывание выполняется на этапе компиляции, то говорят о статическом связывании, если на этапе выполнения – о динамическом связывании. Таким образом, полиморфизм возможен только при динамическом связывании 54 .
В заключение отметим, что в более широком значении, вне контекста ООП, выделяют три типа полиморфизма. Первый тип, который мы рассматриваем в настоящем параграфе, называют полиморфизмом подтипов, или «подтипизацией» ( subtyping ). Другой тип полиморфизма – параметрический полиморфизм, используется в рамках обобщённого программирования. Эта тема будет обзорно рассмотрена в следующей главе. И к третьему типу – ad hoc полиморфизму – относят перегрузку методов, исходя из логики, что одно и то же имя метода в зависимости от параметров может обозначать разные реализации.
§ 43. Принципы качественного проектирования иерархических типов. Объектно-ориентированный язык, как и любой другой язык программирования – инструмент в руках программиста, который можно применять лучше или хуже. В главе 2.6 мы говорили, что не всякое разбиение программы на части, в частности, на классы, будет удачным.
Можно сказать (мы уже формулировали ранее эту идею), что основная цель, для достижения которой формулируются различные принципы (правила) качественного проектирования (design principles) заключается в том, чтобы любой фрагмент кода зависел от другого кода тогда и только тогда, когда это абсолютно необходимо (синтаксически и семантически) для решаемой задачи. Мы рассматривали несколько ключевых правил без учета специфики наследования классов. Рассмотрим теперь два важных правила, относящихся к наследованию.
Начнем с классической задачи, называемой проблемой квадрата-прямоугольника 55 . Положим, у нас есть классы квадрата Square и прямоугольника Rectangle . Логично считать, что квадрат – это разновидность прямоугольника, ведь именно так оно и есть с точки зрения геометрии. Соответственно, класс Square представляется логичным сделать производным от класса Rectangle . Однако рассмотрим следующую реализацию:
class Rectangle < public void SetWidth (float width) < /* . */ >public void SetHeight (float height) < /* . */ >> class Square : Rectangle < >// Использование: Rectangle[] rect = new Rectangle[10]; /* . */ // Растягиваем все прямоугольники по ширине и сжимаем по высоте. float k = 2; for (int i = 0; i < rect.Length; i++) < rect[i].SetWidth(rect[i].GetWidth() * k); rect[i].SetHeght(rect[i].GetHeight() / k) >
Этот код демонстрирует серьезную проблему: методы базового класса Rectangle оказываются неподходящим для производного Square . Дело в том, что квадрат, будучи разновидностью прямоугольника с точки зрения математики, не является разновидностью прямоугольника с точки зрения объектной модели в объектно-ориентированном программировании. Полиморфизм, позволяя работать с объектами базовых типов, не зная реального типа объекта, предполагает, что любой метод базового типа имеет одну и ту же семантику для любого из производных классов. В рассмотренном примере класс Square меняет семантику методов SetWidth и SetHeight . Например, реализуя их так, что вызов любого из них ведет к обновлению и ширины, и высоты. Это изменение приводит к невозможности безопасно использовать переменные базового класса без оглядки на то, к какому именно реальному типу относится объект. Таким образом, сформулируем следующий принцип (правило) объектно-ориентированного проектирования:
Производные классы не должны сужать возможности базовых классов или менять семантику состояния и поведения базовых классов; или, другая формулировка: в любой ситуации, где используется переменная базового типа, должно быть возможно безопасно, то есть без каких-либо изменений в работе программы, заменить ее или присвоить ей экземпляр любого производного класса.
Этот принцип называется принципом подстановки Лисков (Liskov substitution), по фамилии известного американского специалиста Барбары Лисков, которая сформулировала его в 1987 г 56 .
Приведенное решение с квадратом и прямоугольником нарушает этот принцип, так как код с переменной rectangle становится некорректным, если эта переменная имеет реальный тип Square , из-за того что этот тип сужает поведение и меняет семантику состояния и поведения базового класса.
На правах профессионального фольклора приведем еще один пример, демонстрирующий нарушение принципа подстановки. Положим, у нас в программе есть класс птиц Bird , у которого есть метод Fly ( float height ) (не важно какой: конкретный или абстрактный или виртуальный). От этого класса наследуются классы разных птиц: чиж Siskin , стриж Swift и другие.
А теперь мы решили создать класс утка Duck . Утка – птица, но она не умеет летать. При вызове метода Fly программа поведет себя некорректно. Читатель, конечно, может справедливо заметить, что утка всё-таки умеет летать, а некоторые виды уток летают очень хорошо, высоко и далеко. Поэтому в некоторых изложениях утку заменяют пингвином.
Или, другой вариант: есть базовый класс утка Duck и производный класс механическая утка на батарейках ElectroDuck . При этом утка может полететь всегда, а на батарейках – только если батарейки заряжены. Во всех вариантах производный класс сужает поведение базового класса, нарушая принцип подстановки.
Другой принцип проектирования, также имеющий непосредственное отношение к наследованию и полиморфизму, мы уже рассматривали в предыдущем разделе – принцип инверсии зависимости (dependency inversion).
Необходимо всегда использовать наиболее абстрактный (базовый) класс, а не наиболее конкретный.