Опубликован: 04.04.2012 | Доступ: свободный | Студентов: 1988 / 60 | Оценка: 4.60 / 4.40 | Длительность: 13:49:00
Лекция 5:

Универсальность плюс наследование

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >

Обнаружение фактического типа

Помните вопрос, задаваемый в начале этой лекции: "Что, если я знаю, что последний элемент списка является экземпляром TAXI, а не просто VEHICLE, и хочу применить к нему операцию, специфическую для такси"? Обсуждение вклада динамического связывания в архитектуру ПО объясняет, почему для полиморфных структур данных нет срочной необходимости в его рассмотрении.

 Полиморфный список

Рис. 4.3. Полиморфный список

Вы рассматриваете элемент списка, известного вам как список транспортных средств:

fleet: LIST [VEHICLE]

Такое объявление отвергает специфику — идентичность, характерную для такси, автобуса или трамвая, — перед нами просто транспортные средства. У такого подхода есть достоинства и недостатки. Недостаток в том, что нельзя использовать преимущества знания "трамвайности" или "таксичности". Достоинство подхода в том, что можно применить операцию, допустимую для транспортных средств, без знания того, какое именно средство используется в данном случае, даже если эта операция выполняется специфическим образом для каждого вида транспортного средства. Так что всякий раз, когда мы хотим применить специфическую операцию, обычно необходимо сделать эту операцию применимой для всех транспортных средств и задать специфическую реализацию для каждого вида. Это единственный способ справиться с синдромом множества явных вариантов.

Такой подход неприменим в двух случаях.

  • Операция, которую мы хотим использовать, настолько специфична для выбранного типа, что ее никак нельзя применить на более высоком уровне иерархии.
  • Ваша программа получает объекты из внешнего мира, из источника, не подлежащего вашему контролю, — из файла, базы данных, сети. Единственное, что известно, — что объекты могут быть самого общего типа (для нас это тип ANY ).

Рассмотрим важный пример второго случая. Любое хорошее ОО-окружение поддерживает механизм сериализации — записи структуры объектов в файл, и десериализации — получения их оттуда.

retrieved: ANY
    — Объект, полученный последней операцией десериализации

Объект можно объявить только типа ANY, поскольку операция десериализации общецелевая. Она должна работать для любой проблемной области и возвращать любой извлекаемый из файла объект, будь то такси, трамвай, город или еще что-нибудь. В конкретном приложении и для конкретного файла можно ожидать определенный тип объекта, например, такси, но нет возможности, задав t: TAXI, просто написать:

t := my_serializer.retrieved

Тип ANY не согласован с TAXI— справедливо лишь обратное утверждение. В этой ситуации необходимо явное преобразование, называемое кастингом или приведением типа. Зная, что полученный объект является такси, мы должны вернуть ему индивидуальность, отвергнув идентификацию его как ANY, присущую всем объектам в этой толпе. Для поддержки процесса идентификации нам необходимо нечто большее, чем наследование, полиморфизм и динамическое связывание.

В поисках нового механизма заметим, что кастинг не может быть безусловным. Когда объект приходит из файла или по сети, можно ожидать, что он принадлежит определенному типу, но быть уверенным в этом нельзя. Теперь мы имеем дело с объектами, не подлежащими полному контролю в программе. Когда объекты создаются программой, объявление t: TAXI гарантирует, что t всегда будет присоединен к экземпляру TAXI. Для объектов, приходящих из дикого внешнего мира, таких гарантий нет.

Даже, если структура объектов была сериализована вами, при ее десериализации нет гарантии, что она сохранила девственность, — файл мог быть поврежден случайно или злонамеренно. Отсюда для релевантного механизма языка следует правило:

Почувствуй методологию

Принцип Кастинга

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

Другими словами, такой механизм должен быть условным. Если вы ожидаете TAXI, а обнаружили некий другой тип, не согласованный с такси, попытка кастинга должна закончиться неуспехом.

Механизм, не удовлетворяющий этому принципу, свойственен кастингу, реализованному в языке С (в языке С++ принцип также не выдерживается, но там есть более изощренная форма приведения к типу, соответствующая принципу). В языке С, написав (T) e, где T - тип, а e - ссылка, получим ссылку на объект типа T с сохранением значения e, независимо от фактического типа данных, хранящихся в соответствующей памяти. Это прямое следствие машинного уровня языка, который рассматривает ссылку (указатель) просто как адрес памяти, а не как ссылку на объект определенного типа. Логика такова: "программисты знают, что они делают".

Общим термином для описания механизма кастинга, удовлетворяющего принципу, является " динамический кастинг" (приемлем также термин "условный кастинг"). Это еще одна область, где отсутствует стандартная терминология. Можно встретить в литературе "сужение типа" (narrowing) или "приведение вниз" (downcasting). Эти термины используются, когда речь идет о приведении общего типа к специальному типу, например, от VEHICLE к TAXI. Такой случай часто встречается на практике, но он не единственный. Мы увидим, почему изучаемый механизм может применять динамический кастинг к объектам произвольных типов.

Наиболее общий термин, покрывающий все случаи нахождения типа объектов во время выполнения, носит название RTTI (Run Time Type Identification).

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

Давайте рассмотрим два механизма динамического кастинга, один — текущий, другой — устаревший, но все еще используемый.

Тест объекта

 Полиморфный список

Рис. 4.4. Полиморфный список

Предположим, вы убеждены, что последний объект fleet — это объект TAXI, как на рисунке. Потому вы хотите применить специфический компонент, такой, как take, к этому элементу. Простейшее решение не будет работать:

fleet.last.take (...)

Причина этого должна быть ясна, но ее можно проанализировать тщательнее, если разделить выполнение оператора на части:

— Предыдущие объявления:
fleet: LIST[VEHICLE]
t:?    — Держатель места, должен быть заменен фактическим типом
...
t := fleet.last
Листинг 4.2.
t.take (...)
Листинг 4.3.

При объявлении t использован знак вопроса вместо фактического типа. Причина в том, что возникает неразрешимая дилемма.

  • Если объявить t как VEHICLE, то выражение будет правильной конструкцией, а выражение нет, так как take не является компонентом VEHICLE.
  • Если объявить t как TAXI, то выражение будет правильной конструкцией, но не выражение, так как нарушается правило полиморфизма типов: потомку нельзя присваивать родительский объект — это противоречит направлению согласования типов.

Конструкция теста объекта обеспечивает решение в таких случаях. Тест объекта — это булевское выражение, которое (как форма RTTI) определяет, согласован ли тип объекта с ожидаемым типом. В случае согласования возникает дополнительный эффект — появление локальной переменной, присоединенной к объекту. Применяя тест объекта в нашем примере, получим:

if attached (TAXI} fleet.last as t then
  t.take (. )
   ... Любая другая TAXI-операция над t, присоединенному к TAXI-объекту...
else
  ...Делай что-нибудь еще (не над такси), если необходимо...
end

В соответствии с принципом кастинга операция является условной. Она тестирует тип fleet.last — фактического объекта, присоединенного к ссылке во время выполнения.

Возвращается значение false, если нет согласования с заданным типом TAXI. В этом случае будет выполняться else -ветвь данного оператора, если она присутствует. Если же тип согласован, то в нашем распоряжении появляется объект такси, и он становится локально доступным через заданное имя t, так называемую локальную переменную теста объекта. По семантике это соответствует корректному объявлению t и присвоению ему значения, как в выражении.

Если тест объекта служит условием оператора if, как в данном случае, то областью локальной переменной теста является then -ветвь, где можно использовать t как переменную TAXI, обозначающую значение fleet.last. Поскольку это и в самом деле TAXI, что установлено как статически в результате объявления, так и динамически — в результате проверки присоединенного объекта во время выполнения, то без всякого риска можно применять к t операции, специфические для такси.

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

  • Как мы видели, если тест объекта появляется как условие if, то областью является then -ветвь оператора. Сюда же включается случай, когда тест комбинируется с другими условиями через and then, как if attached (TAXI) fleet.item as t and then v.ismoving then...
  • Если используется отрицание теста (возможно, комбинируемое с другими условиями через связку or else ), то областью является else -ветвь, как if not attached (TAXI) fleet.item as t then.
  • Аналогично, если тест появляется с отрицанием в условии выхода из цикла — предложении until, то областью является тело цикла. И снова возможна комбинация теста с другими булевскими выражениями.

Как пример последнего случая, рассмотрим, как подсчитать число объектов, предшествующих первому объекту специального типа в полиморфном списке.

pre_taxi_count (fleet: LIST [VEHICLE]): INTEGER
    — Число объектов fleet, перед первым TAXI
  do
    from fleet.start until
      fleet.after or else attached (TAXI) fleet.item as t
    loop
      Result := Result +1 ; fleet.forth
    end
  ensure
    non_negative: Result >= 0
    at_most_length_of_list: Result <= fleet.count
  end

Этот конкретный алгоритм не использует локальную переменную теста t ; как следствие, тест можно записывать в этом случае проще, как attached (TAXI) fleet.item.

Ограничений на типы, включаемые в тест, не существует: attached! U exp as x, где exp является выражением (статического) типа T — типы U и T могут быть произвольными. В наиболее распространенном случае, названном "приведением вниз", тип U является потомком типа T. Пример с типами VEHICLE и TAXI иллюстрирует эту ситуацию. Но это не является абсолютным требованием, и для множественного наследования может понадобиться тест для типов U и T, не связанных отношением наследования. Типичный пример показан на следующем рисунке.

 Непрямые отношения наследования

Рис. 4.5. Непрямые отношения наследования

Классы NUMERIC и HASHABLE разделяют потомков, таких как B и C. Если имеется список numlist из элементов NUMERIC, то может потребоваться хеширование тех элементов списка, которые допускают эту операцию:

if attached (HASHABLE} numlist.item as h then
  your_hash_table.put (h, h.hash_code)
end

В этом фрагменте numlist имеет тип LIST[NUMERIC], а hashcode — это метод класса HASHABLE. Приведение типов в данном случае не является приведением вниз, так как HASHABLE не согласован с NUMERIC. Случай вполне законный и реально существующий. Он иллюстрирует, почему необходим общий механизм динамического кастинга, а не ограниченное сужение типа.

Попытка присваивания

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

fleet: LIST [VEHICLE]
t:TAXI
— t : = fleet.last    — Только для сравнения: закомментировано, поскольку
                                        — неверно!
t?= fleet.last
t.take (. )    — Правильно, но не безопасно, смотри далее

Мы объявили t как TAXI, что делает t : = fleet.last неверным: присваивание в направлении, ошибочном для согласования типов. Вместо обычного присваивания (:=) давайте будем использовать "попытку присваивания", использующую символы "?=". Семантика такова:

  • если во время выполнения значение источника (правая сторона присваивания) согласовано по типу с типом цели (здесь TAXI), то цель, здесь t, присоединяется к объекту как в обычном присваивании;
  • в противном случае t получает значение void.

Безопасное использование попытки присваивания должно непосредственно после попытки присваивания и до использования полученной переменной как цели вызова компонента выполнить "тест на void ":

t?= fleet.last if t /= Void then
  t.take (. )    — Правильно и безопасно
   ... Любая другая TAXI операция над t, присоединенному к TAXI-объекту...
else
   ... Делай что-нибудь еще (не над такси), если необходимо...
end

Ясно, что попытку присваивания можно проводить всюду, где может использоваться тест объекта. Новый механизм имеет преимущество не по причине загромождения теста локальными переменными, такими как t, — локальная переменная теста играет ту же роль. Но дело в том, что она появляется точно в том месте, где необходимо ее использовать. Помимо этого, использование Void в качестве ошибки является небезопасным, так как ничто не заставляет вас выполнять тест на void. Отсутствие проверки приводит к void -вызову со всеми вытекающими последствиями, а при применении теста объекта этого не происходит. В результате честно служившая в течение десятилетий попытка присваивания " подала в отставку", и теперь в стандарте языка Eiffel используется тест объекта.

Динамический кастинг используйте "с умом"

Следует всегда помнить о маячащей угрозе синдрома множества явных вариантов. Динамический кастинг делает возможным реализовать структуру решения в форме: "Если TAXI, то делай это, иначе, если TRAM, то делай то, иначе, если BUS.".

Детали, кстати, должны быть тщательно учтены — по той причине, что тесты используют согласование типов. Если выполняется разбор случаев MOVING -объектов, то следует помнить, что, например, объект TRAM согласован также с типом VEHICLE, так что порядок тестов играет значение.

Все же вы можете делать это.

Ясно, что это не лучшая идея. Слова, поясняющие, почему это плохо, уже сказаны. Динамический кастинг полезен, но не как соперник динамическому связыванию, который побеждает в схватке, когда оба соперника применимы. Динамический кастинг необходим, когда объекты приходят из внешнего мира — файлов, баз данных, сети, чьи типы программа должна анализировать динамически перед их использованием.

В таких случаях часто приведение делается к одному типу, не выполняя проверки всех возможных случае"приведение вниз"Разумно используйте динамический кастинг. Это довольно хороший критерий использования динамического кастинга. Если ожидается некоторый тип объекта и выясняется, что реальность согласована с ожиданиями, то все прекрасно. Если же приходится делать выбор, анализируя весь возможный набор типов, то почти наверняка динамический кастинг используется не по назначению и его место должно занять динамическое связывание.

Есть еще один выход. Если нет свободы в создании новых классов, то применяйте образец "Посетитель", описание которого является заключительной темой этой лекции.

< Лекция 4 || Лекция 5: 1234 || Лекция 6 >
Надежда Александрова
Надежда Александрова

Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения".