Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
С# и объектно-ориентированное программирование.doc
Скачиваний:
1
Добавлен:
01.05.2025
Размер:
1.3 Mб
Скачать
    1. Абстрактные классы

В настоящее время базовый класс Employee спроектирован так, что поставляет различные данные-члены своим наследникам, а также предлагает два виртуальных метода (GiveBonus() и DisplayStatus()), которые могут быть переопределены наследниками. Хотя все это хорошо и замечательно, у данного дизайна есть один неприятный побочный эффект: можно непосредственно создавать экземпляры базового класса Employee:

// Что это будет означать?

Employee X = new Employee() ;

В нашем примере базовый класс Employee имеет единственное назначение — определить общие члены для всех подклассов. По всем признакам вы не намерены позволять кому-либо создавать прямые экземпляры этого класса, поскольку тип Employee слишком общий по своей природе. «Просто сотрудников» у нас быть не должно – каждой из возможных категорий сотрудников соответствует свой класс.

Учитывая то, что многие базовые классы склонны быть довольно неопределенными сущностями, намного лучший дизайн для рассматриваемого примера не должен разрешать непосредственное создание в коде нового объекта Employee. В С# можно добиться этого с использованием ключевого слова abstract в определении класса, создавая, таким образом, абстрактный базовый класс:

(этот код вставляем в проект EmployeeExample, класс Employee, добавляем abstract в описании класса)

Задание!!!: протестировать, исправить возможные ошибки

После этого попытка создать экземпляр класса Employee приведет к ошибке во время компиляции:

// Ошибка! Нельзя создавать экземпляр абстрактного класса!

Employee X = new Employee();

На первый взгляд может показаться очень странным, зачем определять класс, экземпляр которого нельзя создать непосредственно.

Базовые классы (абстрактные или нет) очень полезны тем, что содержат общие данные и общую функциональность, которую могут использовать наследуемые типы.

Также следует понимать, что хотя непосредственно создать абстрактный класс нельзя, он все же присутствует в памяти, когда создан экземпляр его производного класса. Таким образом, совершенно нормально (и принято) для абстрактных классов определять любое количество конструкторов, вызываемых опосредованно при размещении в памяти экземпляров производных классов.

    1. Полиморфный интерфейс

Когда класс определен как абстрактный базовый (с помощью ключевого слова abstract), в нем может определяться любое количество абстрактных членов. Абстрактные члены могут использоваться везде, где необходимо определить член, которые не предлагает реализации по умолчанию. За счет этого вы навязываете полиморфный интерфейс каждому наследнику, возлагая на них задачу реализации конкретных деталей абстрактных методов. Полиморфный интерфейс абстрактного базового класса просто ссылается на его набор виртуальных и абстрактных методов.

Данная особенность ООП позволяет строить легко расширяемое и гибкое программное обеспечение.

Пример 2.

Для иллюстрации полиморфизма рассмотрим иерархию фигур.

Для начала создадим новое консольное приложение С# по имени ShapeExample.

(этот код вставляем в проект ShapeExample, пространство имен Shapes)

Задание!!!: протестировать, исправить возможные ошибки

Обратите внимание что типы Hexagon и Circle расширяют базовый класс Shape. Подобно любому базовому классу, в Shape определен набор членов (в данном случае свойство PetName и метод Draw()), общих для всех наследников. Подобно иерархии классов сотрудников, нужно запретить непосредственное создание экземпляров Shape, поскольку этот тип представляет слишком абстрактную концепцию. Чтобы предотвратить прямое создание экземпляров Shape, можно определить его как абстрактный класс. Также, учитывая, что производные типы должны уникальным образом реагировать на вызов метода Draw(), пометим его как virtual и определим реализацию по умолчанию.

(этот код вставляем в проект ShapeExample, пространство имен Shapes)

Задание!!!: протестировать, исправить возможные ошибки

Польза от абстрактных методов становится совершенно ясной, как только приходит понимание, что подклассы никогда не обязаны переопределять виртуальные методы (как в случае Circle). Поэтому если создать экземпляр типа Hexagon и Circle, обнаружится, что Hexagon знает, как правильно "рисовать" себя (или, по крайней мере, выводит на консоль соответствующее сообщение). Однако реакция Circle иная:

(этот код вставляем в проект ShapeExample, метод Main)

Задание!!!: протестировать, исправить возможные ошибки

Вывод этого метода Main() выглядит следующим образом:

Ясно, что это не особо интеллектуальный дизайн для текущей иерархии. Чтобы заставить каждый класс переопределить метод Draw(), можно определить Draw() как абстрактный метод класса Shape, а это означает отсутствие какой-либо реализации по умолчанию. Для пометки метода как абстрактного в С# служит ключевое слово abstract, абстрактные методы не предусматривают вообще никакой реализации:

(этот код вставляем в проект ShapeExample, класс Shape)

Задание!!!: протестировать, исправить возможные ошибки

Важно! Абстрактные методы могут определяться только в абстрактных классах. Попытка поступить иначе приводит к ошибке во время компиляции.

Методы, помеченные как abstract, являются чистым протоколом. Они просто определяют имя, возвращаемый тип (если есть) и набор параметров (при необходимости). Здесь абстрактный класс Shape информирует типы-наследники о том, что у него есть метод по имени Draw(), который не принимает аргументов и ничего не возвращает. О необходимых деталях должен позаботиться наследник. С учетом этого метод Draw() в классе Circle теперь должен быть обязательно переопределен. В противном случае Circle также должен быть абстрактным типом и оснащен ключевым словом abstract (это очевидно не подходит в данном примере). Ниже показаны необходимые изменения в коде:

(этот код вставляем в проект ShapeExample, класс Circle)

Задание!!!: протестировать, исправить возможные ошибки

Теперь делается предположение о том, что любой унаследованный от Shape класс должен иметь уникальную версию метода Draw(). Для демонстрации полной картины полиморфизма рассмотрим следующий код:

(этот код вставляем в проект ShapeExample, метод Main)

Задание!!!: протестировать, исправить возможные ошибки

Ниже показан вывод этого метода Main():

Этот метод Main () иллюстрирует использование полиморфизма в чистом виде. Хотя невозможно напрямую создавать экземпляры абстрактного базового класса (Shape), можно свободно сохранять ссылки на объекты любого подкласса в абстрактной базовой переменной. Таким образом, созданный массив объектов Shape может хранить объекты, унаследованные от базового класса Shape (попытка поместить в массив объекты, несовместимые с Shape, приводит к ошибке во время компиляции).

Учитывая, что все элементы в массиве my Shapes действительно наследуются от Shape, известно, что все они поддерживают один и тот же полиморфный интерфейс (или, говоря конкретно — все они имеют метод Draw()). Выполняя итерацию по массиву ссылок Shape, исполняющая система сама определяет, какой конкретный тип имеет каждый его элемент. И в этот момент вызывается корректная версия метода Draw(). Эта техника также делает очень простой и безопасной задачу расширения текущей иерархии. Например, предположим, что от абстрактного базового класса Shape унаследовано еще пять классов (Triangle, Square и т.д.). Благодаря полиморфному интерфейсу, код внутри цикла foreach не потребует никаких изменений, если компилятор увидит, что в массив myShapes помещены только Shape-совместимые типы.