лекция 21 (1161116), страница 2
Текст из файла (страница 2)
Но не всегда удаётся так сделать. Некоторые функции могут иметь двоякий смысл (например, draw - в переводе это рисовать и тянуть, и например, для реализации пасьянса не обходимы обе функции).
Б)В Cи# есть явная реализация метода. Пусть один интерфейс реализует Execute и другой реализует Execute.
Пусть у нас есть класс Х:
class X: IFoo1, IFoo2 {
void IFoo1.Execute() {...} //такое нужно при конфликте имен
void IFoo2.Execute() {...}
};
X x = new X;
((IFoo1)x).Execute();//вызываем первый метод Execute
А если просто написать x.Execute? Вот тогда будет выдана ошибка.
В классе Х мы можем написать свой Execute:
void Execute() {...} //мы должны явным образом что-то перенаправить
И в нём, возможно, вызывать какой-то из родительских Execute или написать наш. Итак, в случае если хочешь реализовать общий Execute, то надо реализовать 1-ый, 2-ой и явно сослаться, какой выбираешь. Но оба Execute должны быть реализованы! Это сделано для того, чтобы при явном приведении к одному из двух интерфейсов мы знали какой из 2-х Execute выбрать. Поскольку мы используем 2 интерфейса, мы должны выполнять 2 контракта.
//В Java все с точностью до наоборот – там мы обязаны реализовать только один Execute(который удовлетворяет требованиям сразу двух контрактов).
При реализации методов Execute не указывается никаких модификаторов, и в этом есть глобальный смысл. По умолчанию эти идентификаторы считаются как public.
Последние замечания к интерфейсам.
Интерфейсы очень полезны, например, для реализации рефлексии (в момент выполнения определять некоторые свойства, определённые в исходном тексте, динамически подключать классы, определять их сигнатуры, осуществлять все проверки, которые можно осуществлять при проверке текста).
Пример1.
using (X x = new X()) //объект х обязан реализовывать интерфейс IDisposable
{
...
}//здесь из IDisposable вызывается метод dispose, который явным образом удаляет объект из динамической памяти
Для того, чтобы использовать наш объект в данной конструкции необходимо, чтобы он явно или косвенно поддерживал интерфейс IDisposable.
Пример2.
foreach (X x in S) {...} //S – некоторая коллекция
Объекты х по очереди присваивают объекты из коллекции. Нужно чтобы класс объекта х поддерживал некоторый интерфейс, который позволяет перебирать соответствующие коллекции.
В Си# и Java введены много интерфейсов, которые позволяют связать объекты с языковыми конструкциями и позволяют использовать свойства рефлексии.
Дополнительные вопросы
Множественное наследование (МН)
Первая проблема МН - проблема имён.
Си++
В языке Си++ МН выглядит следующим образом:
class X: public B1, public B2, ... //произвольный список классов, модификатор доступа произвольный
{
...
}
Очевидно, что все классы В должны быть различными (из-за проблемы именования).
В Си++ это решается через явную классификацию имени класса (если профили одинаковые):
B1::f(), B2::f()
При единичном наследовании у нас получается некая иерархия. Но при множественном наследовании у нас могут возникать различные структуры, напоминающие графы.
Виртуальные наследование - такое наследование, при котором при наследовании одинаковые родительские классы сливаются друг с другом.
Пример1.
--- схема класса Link (реализует однонаправленный список) ---
Такое наследование не возможно, возникнет конфликт одинаковых имен.
Так сделать можно. Как мы доберемся, например, до функции GetNext?
Если просто GetNext – то из Link, а если х.GetNext – из класса Х. Кроме того здесь есть доминирование – выбирается ближайший класс, который реализует соответствующую функцию. Решаются проблемы с конфликтом имён полным путем по дереву: X::Link::getLink();
Объект класса Y: // два подобъекта класса Link
Link next
X
Link next
Y
Получается, что объект типа Link находится в двух однонаправленных списках. Поля next в двух Link – разные, и никакой речи о виртуальности не идет. Рассмотрим другой пример.
Пример2.
Другой пример иерархии (примерная иерархия класса iostream):
ios <=> int fd //целочисленный файловый дескриптор
1)
З аметим, что ios – не абстрактный класс (функциональность – открывать и закрывать).
Такое наследование называется бриллиантовым или ромбовидным.
Наследование:
ios
istream
ostream
iostream
Сколько в iostream объектов ios? Тут может быть две схемы: одна – указанная выше, и другая.
2)
В этой ситуации у нас два файловых дескриптора: один на чтение, другой на запись.
Но у нас файлы устроены по-другому, у нас в один и тот же дескриптор можно и читать и писать.
Наследование:
ios
istream
ios
ostream
iostream
--- для ромбовидного наследования ---
Мы должны учитывать возможность множественного наследования еще на стадии ответвления istream и ostream; и написать так:
class istream: virtual public ios {...}
class ostream: virtual public ios {...}
Если опустить одно из двух virtual – никакого виртуального наследования не будет.
class iostream: public istream, public ostream //(тут о ключевом слове virtual мы по магическим причинам можем забыть)
При ромбовидном множественном наследовании мы можем получить структуру классов, называемую решёткой (частичный порядок – не каждые два класса могут находиться в отношении родства)1.
Проблемы в МН возникают только если есть виртуальные методы, но зачем нужно наследование без виртуальных методов?! В 90% случаев наследование нужно вместе с динамическим связыванием.
Пример проблемы:
При вызове p->f(); Какой this мы передаём? Тут уже не всё так просто.
Пусть есть:
class Х { // в котором есть вм
}
class У // в котором есть вм
class Z{
}
Ситуация 1. Пусть в Х определена f() и она же переопределяется в Z. Неважно динамический тип р есть Х или Z. То ли функция f() для класса Х будет вызываться, то ли для класса Z – все равно.
Ситуация 2. в У определена f(), а в классе Z она не переопределена. Значит, функция должна вызываться для какого класса? Для У. А какой указатель будет передаваться? На Х.
Если f() передать указатель р, то предастся -> X. Потому как функция f() в классе У не знает что ее унаследовали (Y считает, что this указывает на него, а не на Х). Нужно р+дельта и предать все это в f() в качестве this. Компилятор – этого не знает (т.к. метод динамически связан), и в общем случае компилятор может не знать на что указывает р. Если р указывает на Х или на У – это одно. Пусть у нас У* р – тогда компилятор не знает то ли р указывает на объект класса Z, то ли он указывает на отдельно стоящий объект класса У.
Тут возникает специальная схема реализации множественного наследования, которая основана на том, что у нас должна быть сдвоенная ТВМ.
ссылка на ТВМ XZ |
Х |
ссылка на ТВМ YZ |
У |
Z |
Кроме этого в специальных ТВМ содержится специальная дельта*, которую нужно прибавить к указателю this, чтобы получить нужный указатель. Для XZ дельта* будет нулевая, а для YZ дельта* будет содержать ту самую дельту, о которой говорилось ранее. Получается не самая красивая схема.
Все проблемы связаны только с наследованием данных (возникают большие накладные расходы даже при эффективной реализации). Если речь идет только о наследовании интерфейсов, то мы можем просто построить ВМ для Х, ВМ для У, ВМ для Z. Если У и последующие базы у нас не содержит никаких данных, то мы можем построить ТВМ именно таким образом:
Х |
У |
Z |
Объединенные таблицы нужны для дельта (прибавлять ее или нет). Если в Z не переопределяется функция из У, тогда дельта должна стоять, а если переопределяется то дельта = 0. А если никаких данных нет, то и дельты нет.
Итак для МН:
-
трудности реализации
-
накладные расходы
-
усложняются программы
Поэтому МН в современных яп реализовывается для интерфейсов, где никаких проблем нет.
1 Но за всё в этой жизни надо платить. Страуструп писал, что он совершил только единственную дизайнерскую ошибку. Он начал реализацию эффективного мн противовес шаблоном. В итоге шаблоны появились в языке много позже. И по многих коммерческих компиляторах механизм шаблонов до сих пор не реализован до конца. Библиотека STL появилась только в 90-ых годах, одна и та же реализация STL не может одинаково компилироваться. Говорят не о библиотеки STL, а о платформе STL – это собственно реализация STL + компилятор.