Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
public override void DisplayStats()
{
base.DisplayStats();
}
Запечатывание виртуальных членов
Вспомните, что к типу класса можно применить ключевое слово sealed, чтобы предотвратить расширение его поведения другими типами через наследование. Ранее класс PtSalesPerson был запечатан на основе предположения о том, что разработчикам не имеет смысла дальше расширять эту линию наследования.
Следует отметить, что временами желательно не запечатывать класс целиком, а просто предотвратить переопределение некоторых виртуальных методов в производных типах. В качестве примера предположим, что вы не хотите, чтобы продавцы с частичной занятостью получали специальные бонусы. Предотвратить переопределение виртуального метода GiveBonus() в классе PtSalesPerson можно, запечатав данный метод в классе SalesPerson:
// Класс SalesPerson запечатал метод GiveBonus()!
class SalesPerson : Employee
{
...
public override sealed void GiveBonus(float amount)
{
...
}
}
Здесь класс SalesPerson на самом деле переопределяет виртуальный метод GiveBonus(), определенный в Employee, но явно помечает его как sealed. Таким образом, попытка переопределения метода GiveBonus() в классе PtSalesPerson приведет к ошибке на этапе компиляции:
sealed class PTSalesPerson : SalesPerson
{
...
// Ошибка на этапе компиляции! Переопределять этот метод
// в классе PtSalesPerson нельзя, т.к. он был запечатан.
{
}
}
Абстрактные классы
В настоящий момент базовый класс Employee спроектирован так, что поставляет различные данные-члены своим наследникам, а также предлагает два виртуальных метода (GiveBonus() и DisplayStats()), которые могут быть переопределены в наследниках. Хотя все это замечательно, у такого проектного решения имеется один весьма странный побочный эффект: создавать экземпляры базового класса Employee можно напрямую:
// Что это будет означать?
Employee X = new Employee();
В нашем примере базовый класс Employee служит единственной цели — определять общие члены для всех подклассов. По всем признакам мы не намерены позволять кому-либо создавать непосредственные экземпляры типа Employee, т.к. он концептуально чересчур общий. Например, если кто-то заявит, что он сотрудник, то тут же возникнет вопрос: сотрудник какого рода (консультант, инструктор, административный работник, литературный редактор, советник в правительстве)?
Учитывая, что многие базовые классы имеют тенденцию быть довольно расплывчатыми сущностями, намного более эффективным проектным решением для данного примера будет предотвращение возможности непосредственного создания в коде нового объекта Employee. В C# цели можно добиться за счет использования ключевого слова abstract в определении класса, создавая в итоге абстрактный базовый класс:
// Превращение класса Employee в абстрактный для
// предотвращения прямого создания его экземпляров.
abstract partial class Employee
{
...
}
Теперь попытка создания экземпляра класса Employee приводит к ошибке на этапе компиляции:
// Ошибка! Нельзя создавать экземпляр абстрактного класса!
Employee X = new Employee();
Определение класса, экземпляры которого нельзя создавать напрямую, на первый взгляд может показаться странным. Однако вспомните, что базовые классы (абстрактные или нет) полезны тем, что содержат все общие данные и функциональность для производных типов. Такая форма абстракции дает возможность считать, что "идея" сотрудника является полностью допустимой, просто это не конкретная сущность. Кроме того, необходимо понимать, что хотя непосредственно создавать экземпляры абстрактного класса невозможно, они все равно появляются в памяти при создании экземпляров производных классов. Таким образом, для абстрактных классов вполне нормально (и общепринято) определять любое количество конструкторов, которые вызываются косвенно, когда выделяется память под экземпляры производных классов.
На данной стадии у нас есть довольно интересная иерархия сотрудников. Мы добавим чуть больше функциональности к приложению позже, при рассмотрении правил приведения типов С#. А пока на рис. 6.4 представлено текущее проектное решение.
Полиморфные интерфейсы
Когда класс определен как абстрактный базовый (посредством ключевого слова abstract), в нем может определяться любое число абстрактных членов. Абстрактные члены могут применяться везде, где требуется определить член, который не предоставляет стандартной реализации, но должен приниматься во внимание каждым производным классом. Тем самым вы навязываете полиморфный интерфейс каждому наследнику, оставляя им задачу реализации конкретных деталей абстрактных методов.
Выражаясь упрощенно, полиморфный интерфейс абстрактного базового класса просто ссылается на его набор виртуальных и абстрактных методов. На самом деле это намного интереснее, чем может показаться на первый взгляд, поскольку данная характерная черта ООП позволяет строить легко расширяемые и гибкие приложения. В целях иллюстрации мы реализуем (и слегка модифицируем) иерархию фигур, кратко описанную в главе 5 во время обзора основных принципов ООП. Для начала создадим новый проект консольного приложения C# по имени Shapes.
На рис. 6.5 обратите внимание на то, что типы Hexagon и Circle расширяют базовый класс Shape. Как и любой базовый класс. Shape определяет набор членов (в данном случае свойство PetName и метод Draw()), общих для всех наследников.
Во многом подобно иерархии классов для сотрудников вы должны иметь возможность запретить создание экземпляров класса Shape напрямую, потому что он представляет слишком абстрактную концепцию. Чтобы предотвратить непосредственное создание экземпляров класса Shape, его можно определить как абстрактный класс. К тому же с учетом того, что производные типы должны уникальным образом реагировать на вызов метода Draw(), пометьте его как virtual и определите стандартную реализацию. Важно отметить, что конструктор помечен как protected, поэтому его можно вызывать только в производных классах.
// Абстрактный базовый класс иерархии.
abstract class Shape
{
protected Shape(string name = "NoName")
{ PetName = name; }
public string PetName { get; set; }
// Единственный виртуальный метод.
public virtual void Draw()
{
Console.WriteLine("Inside Shape.Draw()");
}
}
Обратите внимание, что виртуальный метод Draw() предоставляет стандартную