Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
// В классе Circle метод Draw() НЕ переопределяется.
class Circle : Shape
{
public Circle() {}
public Circle(string name) : base(name){}
}
// В классе Hexagon метод Draw() переопределяется.
class Hexagon : Shape
{
public Hexagon() {}
public Hexagon(string name) : base(name){}
public override void Draw()
{
Console.WriteLine("Drawing {0} the Hexagon", PetName);
}
}
Полезность абстрактных методов становится совершенно ясной, как только вы снова вспомните, что подклассы никогда не обязаны переопределять виртуальные методы (как в случае Circle). Следовательно, если создать экземпляры типов Hexagon и Circle, то обнаружится, что Hexagon знает, как правильно "рисовать" себя (или, по крайней мере, выводить на консоль подходящее сообщение). Тем не менее, реакция Circle порядком сбивает с толку.
Console.WriteLine("***** Fun with Polymorphism *****n");
Hexagon hex = new Hexagon("Beth");
hex.Draw();
Circle cir = new Circle("Cindy");
// Вызывает реализацию базового класса!
cir.Draw();
Console.ReadLine();
Взгляните на вывод предыдущего кода:
***** Fun with Polymorphism *****
Drawing Beth the Hexagon
Inside Shape.Draw()
Очевидно, что это не самое разумное проектное решение для текущей иерархии. Чтобы вынудить каждый дочерний класс переопределять метод Draw(), его можно определить как абстрактный метод класса Shape, т.е. какая-либо стандартная реализация вообще не предлагается. Для пометки метода как абстрактного в C# используется ключевое слово abstract. Обратите внимание, что абстрактные методы не предоставляют никакой реализации:
abstract class Shape
{
// Вынудить все дочерние классы определять способ своей визуализации.
public abstract void Draw();
...
}
На заметку! Абстрактные методы могут быть определены только в абстрактных классах, иначе возникнет ошибка на этапе компиляции.
Методы, помеченные как abstract, являются чистым протоколом. Они просто определяют имя, возвращаемый тип (если есть) и набор параметров (при необходимости). Здесь абстрактный класс Shape информирует производные типы о том, что у него есть метод по имени Draw(), который не принимает аргументов и ничего не возвращает. О необходимых деталях должен позаботиться производный класс.
С учетом сказанного метод Draw() в классе Circle теперь должен быть обязательно переопределен. В противном случае Circle также должен быть абстрактным классом и декорироваться ключевым словом abstract (что очевидно не подходит в настоящем примере). Вот изменения в коде:
// Если не реализовать здесь абстрактный метод Draw(), то Circle
// также должен считаться абстрактным и быть помечен как abstract!
class Circle : Shape
{
public Circle() {}
public Circle(string name) : base(name) {}
public override void Draw()
{
Console.WriteLine("Drawing {0} the Circle", PetName);
}
}
Итак, теперь можно предполагать, что любой класс, производный от Shape, действительно имеет уникальную версию метода Draw(). Для демонстрации полной картины полиморфизма рассмотрим следующий код:
Console.WriteLine("***** Fun with Polymorphism *****n");
// Создать массив совместимых с Shape объектов.
Shape[] myShapes = {new Hexagon(), new Circle(), new Hexagon("Mick"),
new Circle("Beth"), new Hexagon("Linda")};
// Пройти в цикле по всем элементам и взаимодействовать
// с полиморфным интерфейсом.
foreach (Shape s in myShapes)
{
s.Draw();
}
Console.ReadLine();
Ниже показан вывод, выдаваемый этим кодом:
***** Fun with Polymorphism *****
Drawing NoName the Hexagon
Drawing NoName the Circle
Drawing Mick the Hexagon
Drawing Beth the Circle
Drawing Linda the Hexagon
Данный код иллюстрирует полиморфизм в чистом виде. Хотя напрямую создавать экземпляры абстрактного базового класса (Shape) невозможно, с помощью абстрактной базовой переменной допускается хранить ссылки на объекты любого подкласса. Таким образом, созданный массив объектов Shape способен хранить объекты классов, производных от базового класса Shape (попытка добавления в массив объектов, несовместимых с Shape, приведет к ошибке на этапе компиляции).
С учетом того, что все элементы в массиве myShapes на самом деле являются производными от Shape, вы знаете, что все они поддерживают один и тот же "полиморфный интерфейс" (или, говоря проще, все они имеют метод Draw()). Во время итерации по массиву ссылок Shape исполняющая система самостоятельно определяет лежащий в основе тип элемента. В этот момент и вызывается корректная версия метода Draw().
Такой прием также делает простым безопасное расширение текущей иерархии. Например, пусть вы унаследовали от абстрактного базового класса Shape дополнительные классы (Triangle, Square и т.д.). Благодаря полиморфному интерфейсу код внутри цикла foreach не потребует никаких изменений, т.к. компилятор обеспечивает помещение внутрь массива myShapes только совместимых с Shape типов.
Сокрытие членов
Язык C# предоставляет возможность, которая логически противоположна переопределению методов и называется сокрытием. Выражаясь формально, если производный класс определяет член, который идентичен члену, определенному в базовом классе, то производный класс скрывает версию члена из родительского класса. В реальном мире такая ситуация чаще всего возникает, когда вы создаете подкласс от класса, который разрабатывали не вы (или ваша команда); например, такой класс может входить в состав программного пакета, приобретенного у стороннего поставщика.
В целях иллюстрации предположим, что вы получили от коллеги на доработку класс по имени ThreeDCircle, в котором определен метод Draw(), не принимающий аргументов:
class ThreeDCircle
{
public void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}
Вы полагаете, что ThreeDCircle "является" Circle, поэтому решаете унаследовать его от своего существующего типа Circle:
class ThreeDCircle : Circle
{
public void Draw()
{