Лекция 14 (Лекции (2009) (Саша Федорова))
Описание файла
Файл "Лекция 14" внутри архива находится в папке "Лекции (2009) (Саша Федорова)". Документ из архива "Лекции (2009) (Саша Федорова)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Лекция 14"
Текст из документа "Лекция 14"
11
Лекция 14
Абстрактные классы и интерфейсы
Любой класс - это тип данных. Однако абстрактный класс – это не абстрактный тип данных!
Понятие абстрактного класса возникает довольно быстро, как только мы начинаем пользоваться объектно-ориентированным языком. Сами по себе иерархии классов не нужны без динамического связывания виртуальных меодов(язык без виртуальных методов – это уже не объектно-ориентированный язык).
Абстрактный класс возникает, когда в нескольких классах существуют признаки, общие для всех объектов. Именно эти общие признаки мы выносим в базовый класс, как можно дальше «наверх».
Однако некоторые функциональности могут быть реализованы только в производных классах. Таким образом неизбежно возникает понятие абстрактного класса.
Все языки, включающие понятие динамически свзяанного метода, включают понятие абстактного класса. Исключение: Оберон-2.
(Turbo Pascal 5.5– тоже не было понятия абстрактного класса, но там существовала стандартная процедура Abstract(). В Delphi абстрактный класс появился.) Большинство языкв для обозначения абстрактного класса используют ключевое слово abstract.
C#, Java
Абстрактный класс – это обычный класс со свойствами, признаками и методами, некоторая функциональность которого нам не известна.
Любого объект обладает поведением и состояние. Поведение объекта выражео его методами, а состояние – членами-данными.
Существуют методы и члены-данные, общие для всех – они принадлежат базовому классу, но реализовать некоторые из них мы можем только в конкретном(не астрактном) классе. Мы абстрагируемся от некоторого конкретного поведения.
Пример
Класс Figure. Чем он обладает:
1) точка привязки – есть у любой фигуры
2) все фигуры можно нарисовать, но каждую – по-разному(пример абстрактного поведения, разного для всех)
3)можем двигать фигуру – в принципе, обще для всех фигур. Любая фигура движется, по сути, одинаково(если мы имеем в виду параллельный перенос)
Можно ли реализовать алгоритм рисования для абстрактной фигуры?
Нет! Если фигура, к примеру, точка – это вызов функции рисования одного пикселя. Отрезок же рисуется совсем по-другому.
Move(dx, dy); //достаточно реализовать, например, в базовом классе
{ x+=dx; y+= dy; }
Почему почти во всех языках програмирования существует абстрактный класс. Объекты абстрактного класса, как правило, нельзя создавать. Компилятор Delphi, однако, раззрешает создавать объекты абстактного типа даных. Но выдает предупреждение. К неопределенным методам обращаться нельзя – иначе будет ошибка.
Объектный ко генерируется таким образом, что если мы написали прототип, но не реализовали его, то можно не вызывать эту функцию – и ошибки не будет. А если мы написали прототип виртуальной функции, но не реализовали ее ни в одном «рребенке»(производном классе), то компоновщик будет ругаться.
Попытки создавать объекты абстрактных классов невозможны почти во всех языках, что гарантирует нам меньше ошибо.
В C# и Java, если перед методом стоит ключевое слово abstract
1) реализация данного метода не требуется
2) класс становится абстрактным
3) перед определением класса должен стоять спецификатор abstract.
Замечание
Абстрактный класс и абсрактный тип даных – это ортогональные понятия.
abstract class Base{
abstract public void Draw();
….
};
Класс называется абстактным в нем есть хотя бы один виртуальный метод.
C#
class D: base{
public override void draw(){……};
};
Java
class D: extends base{
public override void draw(){……};
};
//обязательно поставить тело Draw!
Страуструп отказаслся от ключевого слова abstract, и использовал понятие чисто виртуальной функции
virtual прототип =0;
Может как содержать реализацию, так и не содержать.Существует даже контекст, где чисто виртуальная функция будет вызвана – но это приведет к ошибке.
Замечание.
При вызове функции в конструкторе ее виртуальность снимается(тут она не имеет смысла, ведь в конструкторе мы создаем объект).
Пусть у нас есть иерархия X->Y->Z
Класс X: пусть в нем есть функция f
virtual X() { f(); //this->f, через указатель, но компилятор подставит X::f() }
Пусть в классе Y f замещается.
Как мы знаем, конструкторы класов вызываются в порядке иерархии. При вызове функции в конструкторе класса Х функция f замещаться не будет!
На любой вызов нереализованной виртуальной функции вызовется стандартная процедура Abstract, которая повалит всю нашу программу.
Итог: в любом конексте выов чисто виртуальной функции ведет к ошибке, поэтому в большинстве языков создание объекта абстрактного класса запрещено.
Ада 1995, 2005
Если процедура привязана к тегированому типу данных, ее можно сделать абстрактной. При попытке ее вызвать опять же будет выдана ошибка. В Ада есть абстрактные методы, а абстрактных классов нет. Можно считать, что абстрактный класс в Ада – это тегированная запись с хотя бы одним абстрактным методом.
Примеры
Яркий пример неизбежности абстрактного класса – класс-архиватор. Существуют признаки, общие для всех: имя файала, куда он архивируется(либо какой-то поток в памяти, куда упрятываются архивированные данные).
Методы: extract, add, delete.
Все меоды, связанные с форматом архивации – виртуальные. Ведь мы хотим поддерживать разные форматы, а потому имеет смысл переорпределять методы для каждого конкретног формата в соответствющем классе-потомке.
Еще один пример – класс Image, метод FromFile – реализуется по-разному для каждого конкретного формата изображения.
Интерфейсы
Существует ли связь между абстрактным классом и интерфейсом?
Формально это два ортогональных понятия.
В абстрактном классе мы абстрагируемся от реализации методов.
В интерфейсе мы абстрагируемся от всей структуры класса.
Так как и там и там речь идет об абстракции, существуют ситуацииЮ когда абстактный класс превращается в абстрактный тип данных: если все методы скрыты, а открыты только виртуальные методы.
Пример. Множество.
Уже обсуждалось, что во всех индустриальных языках программирования множества реализованы в стандартной библиотеке, так как оптимальной реализации множества не существует. В STL, как мы помним, реализация основана на бинарных деревьях. В стандарте сказано, что сложность реализации должна быть логарифмической. Представим, что STL у нас нет или что мы очень ее не любим и хотим сами реализовать множество.
Очевидно, что в любом множестве должны поддерживаться операции include(const T&) и exclude(const T&).
Предлагается сделать так:
class ISet{
virtual void exclude(const T &)=0;
virtual void include(const T &)=0;
………………………………………..
};
Классы подобного рода принято называть интерфейсами.
Пусть у нас есть такой класс с только чисто виртуальными функциями. Структура его нам не нжна, мы укажем ее в конкретной реализации, в подклассах.
Такой класс называется абстрактным типом данных, если его структура полностью инкапсулирована(настолько, что ее вовсе нет).
Связь между абстрактным классом и абстрактным типом данных выражается именно вв интерфейсах. Интерфейс – это абстрактный класс, доведеннй до абсолюта.
Существует некий метод, который не хочется делать абстрактным. Это деструктор. Если мы не объявим деструктор как виртуальный, то это в 99% случаев будет свидетельствовать об ошибке.
Base * px = new Derived();
px->f();
delete px; //если мы не объявим деструктор как виртуальный, то уничтожится не объект класса Derived, а объект класса Base. Ресурсы освобождены не будут. Эту ошибку будет найти очень сложно, как и любую ошибку, связанную с утечкой ресурсов.
Общее правило: если есть классы с виртуальным методом, делайте деструктор виртуальным.
Итак, пишем:
class ISet{
virtual void exclude(const T &)=0;
virtual void include(const T &)=0;
virtual ~ISet(){};//ПУСТОЙ
};
По умолчанию, если не написать virtual, деструктор создасться невиртуальным.
Чисто виртуальным деструктор быть не может(тогда он вызовется в деструкторе подобъекта и будет ошибка)
Пример.
сlass SList{…………}; //базовый
class SList_set: public ISet, private SList{
{
ISet* Make() { …………..return new SList_set(); }
};
Заметим, что SList наследуется приватно – тот редкий сллучай, когда нужно приватное наследование.
Таким образом, что в С++ нет языкового понятия интерфейса, однако он легко моделируется вышесказанным образом.
В C# и Java понятие интерфейса существует на языковом уровне.
Interface имя {
объявление членов
};
Кроме функций-методов, как мы помним, в C# может появляться еще и свойство.
interface ISample{
int pi; //поле рассматриваетс как статическая константа
Если написать
int pi{gte; set}
то оно превратится в свойство.
Кстати, перед определением свойство может стоять слово virtual. То есть свойства могут быть виртуальными или чисто виртуальными.
В Java вместо get и set есть getpair и setpair.
Методы в интерфейсах объъявляются без модификаторов – все, что внутри интерфейса, обязано быть публичным по умолчанию. Однако protected все-таки может стоять. Модификатор private ставить бесмысенно.
Часто понятие интерфейса заменяют на контракт.
Класс, реализующий интерфейс, остается абстрактным, если не реализует ВСЕ методы.
Интерфейс на С++ можно смоделировать, как мы показали в примере на ээтой странице, при помощи множественного наследования. А множественное наследование всегда можно смоделировать при помощи включения объектов одного класа в другой как членов-данных.
Но множественное наследование удобно тем, что можно использовать несколько интерфейсов.
Заметим: в чистом виде множественное наследование есть только в С++, а C# и Java множественное наследование реализовано только для интерфейсов.
C#
class D: имя, интерфейсы:
Java
Class D extends Base implements именя интерфейсов
Интерфейсы могут содержать внутри себя любые вложенные классы.
Пример(Java)
Interface ISample{
сlass Settings{…} ; //может быть конретным классом
Settings InitData = new Settings();
//подразумевается, что жанное поле будет статическим
Замечание. Первая проблема при множественном наследовании – это конфликт имен. Пусть у нас есть колода карт(ну, не колода конечно, а класс):
interface ICard{
void Draw(); //раздавать колоду
interface IUIControl{
Draw();
}
Пусть наши программные средства умет работать с IUIControl.
Class Sangle implements ICard, IUIControl{//возникает конфликт имен: два имени Draw. Что из них мы должн релизовывать? Выбрать либо первый, либо второй класс.
В С++ такие реализации разрешаются через явное обращение. Вот так:
class D: public I1, I2{
virtual void I1::f(){…………};
virtual void I2::f(){…………};
………………
};
В С# появляется так называемая явная и неявная реализация интерфейсов.
Неявная реализация – это то, что мы всегда называем обычной реализацией.
Пример неявной реализации интерфейсов:
interface ISample{
void f(); };
class CoClass: ISample{
public void f(){………………..}; //если тут не поставить public, компилятор заругается.
};
Явная реализация интерфейсов:
class CoClass2: ISample{
void ISample f() {…………….}
//тут спецификатор public отсутствует, потому что попытка написать тут public карается
};
Как же вызвать данный метод, если он не публичный?
D * px;
px->f(); //непонятно, какой метд я хочу вызывать – ошибка.
Но px->I1::f(); //снова ошибка: попытка вызова чисто виртуальной функции.
Мы же хотим вызвать заместителя для I1. Это длается так:
((I1*)px)->f();
Замечание. Слова «явный» и «неявный» должны относиться к приведению типов, а не к классам(интерфейсам).
CoClass2 x;
(ISample)x.f();//явное приведение
При явной реализации вызов возможен лишь ри явом приведении.