Глава     

Подклассы и подтипы


Разделы

Содержание

Одной из наиболее интересных особенностей объектно-ориентированных языков программирования является тот факт, что фактический тип переменной может не совпадать с типом, заявленным при ее описании. Мы видели это в главе═6, в конце программы, моделирующей бильярд, где переменная типа GraphicalObject в действительности принимала значения типа Ball, Wall или Hole. Это один из аспектов полиморфизма, который мы будем детально обсуждать в главе═14. В традиционных языках программирования, таких как Pascal или C, если переменная описана как integer, то, что бы ни случилось, содержимое части памяти, отведенной под эту переменную, гарантированно будет интерпретироваться как целая величина. В объектно-ориентированном языке переменная, описанная как Window, в действительности может принимать значения GraphicWindow, TextEditWindow или какого-либо другого оконного типа.

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

Еще два термина имеют отношение к обсуждаемому вопросу. Понятие подкласс уже было введено в главе═7. Благодаря подклассам новые компоненты программ разрабатываются на основе уже существующих. Понятие подтипа является более абстрактным. Подтип определяется в терминах поведения, а не структуры. Мы говорим, что тип В есть подтип типа А, если мы можем в любой ситуации подставить экземпляр класса В вместо экземпляра класса А без каких-либо видимых изменений в поведении.

Отметим, что понятие подтипа соответствует нашему идеализированному принципу подстановки, введенному в главе═9. Однако, абстрактно рассуждая, не существует никаких видимых причин для того, чтобы понятия подкласса и подтипа имели какую-то взаимосвязь. Действительно, в языках программирования с динамическими типами данных вроде Smalltalk они и не связаны. Два класса могут иметь общее поведение═≈ например, по отношению к одному и тому же набору сообщений,═≈ но без общей реализации или единого предка (за исключением класса всех объектов Object). Если их реакции на общие сообщения в достаточной степени аналогичны, то один класс может быть с легкостью подставлен вместо другого. Представьте себе класс разреженных массивов, который поддерживает операции класса массивов Array. Тогда в любой алгоритм, предназначенный для работы с массивами, можно подставить разреженный массив.

Большинство объектно-ориентированных языков программирования со строгим контролем типов данных (такие, как Object Pascal и C++) делают размытым различие между подтипами и подклассами в двух отношениях. Во-первых, они разрешают переменным принимать значения другого типа, только когда динамический тип значения является подклассом статического типа. Во-вторых, они предполагают, что фактически все подклассы являются подтипами. Это предположение не всегда справедливо. Поскольку подклассы могут переопределять методы родителей на произвольное новое поведение, то в общем случае не существует никаких гарантий того, что подкласс будет также и подтипом.

10.1. Связывание методов и сообщения

Разделы

В следующем разделе мы обсудим, как выглядит различие между подклассами и подтипами в обсуждаемых нами языках программирования. Прежде всего, однако, мы должны затронуть две проблемы. Первая из них связана с вопросом о способе связывания. Должны ли мы связывать сообщение и метод, основываясь на статическом типе переменной, или следует принять во внимание ее динамический тип? Второй проблемой является вопрос обращения полиморфизма═≈ можно ли присвоить значение переменной, основываясь на ее динамическом, а не на статическом типе? Третий, тесно связанный с данной темой вопрос═≈ а именно о точном значении переопределения методов,═≈ достаточно сложен, поэтому мы отложим его обсуждение до следующей главы.

10.1.1. Связывание методов

Связывание методов и сообщения


Рис.═10.1. Две точки зрения на полиморфную переменную

Существование полиморфной переменной естественным образом подразумевает наличие двух различных представлений о ней (рис.═10.1). Мы можем рассматривать переменную с точки зрения описания (статически) или с точки зрения ее текущего значения (динамически). Это различие становится важным, когда мы встречаем метод, определенный в родительском классе и переопределенный в дочернем. На рис.═10.1, например, показан метод mouseDown, который имеется как у родительского класса (Window), так и у дочернего (TextEditWindow). Когда полиморфная переменная получает сообщение о щелчке мышью, с каким из методов должно связываться это сообщение?

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

10.1.2. Проблема обращения полиморфизма

Связывание методов и сообщения

Принцип подстановки гласит, что переменной, описанной как экземпляр родительского класса, мы можем присвоить значение дочернего типа. Можем ли мы двигаться в другом направлении? То есть, присвоив переменной Y типа Window значение переменной X типа TextEditWindow, сможем ли мы затем присвоить новой переменной Z типа TextEditWindow нашу переменную Y?

Этот вопрос в действительности содержит в себе две тесно связанные проб√лемы. Чтобы проиллюстрировать это, предположим, что мы определяем класс бильярдных шаров Ball и два его подкласса═≈ черные шары BlackBall и белые шары WhiteBall. Далее мы создаем программный эквивалент коробки, в которую мы можем положить двух представителей класса Ball, один из которых


Рис.═10.2. Бильярдный шар, теряющий свою однозначность

(выбранный случайно) затем вынимается обратно (рис.═10.2). Мы опускаем BlackBall и WhiteBall в коробку и смотрим, что получается на выходе.

Вытащенный из коробки объект определенно может рассматриваться как шар Ball и поэтому может быть присвоен переменной, описанной с этим типом. Но будет ли он черным шаром BlackBall? Мы можем задать здесь два вопроса:═1) могу ли я определить, что этот объект принадлежит к типу BlackBall, или нет?═2) какие механизмы необходимы для того, чтобы присвоить это значение переменной дочернего класса BlackBall?

Хотя пример с шарами BlackBall и WhiteBall может показаться надуманным, скрывающаяся за ним проблема является весьма общей. Рассмотрим проектирование классов для часто используемых структур данных: множества, стеки, очереди, списки и т.═п., которые используются, чтобы хранить совокупность объектов. Рекламируемая выгода объектно-ориентированного программирования состоит в производстве многократно используемых компонент программ, и контейнеры для совокупностей данных являются неплохими кандидатами на эту роль. Однако контейнеры при определенных обстоятельствах похожи на машину с выбором шаров. Если программист помещает два различных объекта в набор и позже вынимает оттуда один, как он узнает, что за объект он получил?

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

10.2. Связывание в языках программирования

Разделы

В следующих разделах мы опишем связывание методов в рассматриваемых языках программирования. В частности, будут противопоставляться между собой способы связывания в языках со статическими типами данных (Object Pascal, C++ и Java) и в языках с динамическими типами (Smalltalk и Objective-C).

10.2.1. Связывание в языке Object Pascal

Связывание в языках программирования

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

Хотя язык базируется на статических типах данных, объекты тем не менее несут с собой знания об их собственном динамическом типе. В Object Pascal версии Apple может использоваться встроенная в систему логическая функция Member, которая определяет, является ли идентификатор экземпляром определенного класса. Функция Member использует в качестве параметров ссылку на объект (обычно это просто идентификатор) и имя класса (в терминологии языка Object Pascal═≈ имя типа данных). Она возвращает значение true, если параметр-ссылка является объектом данного типа, и false в противном случае. Например, задан класс животных Animal. Мы хотим определить, является ли переменная fido, относящаяся к типу Animal, экземпляром более узкого подкласса млекопитающих Mammal с помощью следующего теста:

if Member (fido, Mammal) then
  writeln ('fido is a mammal')
else
  writeln ('fido is not a mammal');

Отметим, что функция Member возвращает значение true, даже если fido является представителем еще более узкого класса (такого, как класс собак Dog). Вкупе с приведением типа данных функция Member может быть использована для частичного решения проблемы обращения полиморфизма. Когда шар вынут из ящика, программист может использовать функцию Member для того, чтобы определить, какой это шар═≈ BlackBall или WhiteBall, и соответственно привести его к правильному типу.

В языке программирования Delphi Pascal класс объекта может быть протестирован с помощью оператора is. Он возвращает значение true, когда класс левого аргумента или совпадает с именем класса справа, или же является его подклассом. Оператор as осуществляет безопасное приведение динамического типа данных. Если левый аргумент не является экземпляром правого класса, возникает исключительная ситуация. В противном случае он приводится к типу правого аргумента. Эти механизмы могут использоваться при работе с обращением полиморфизма.

if aBall is BlackBall then
  bBall := aBall as BlackBall
else
  writeln('cannot convert ball to black ball');

Если мы хотим узнать истинный класс объекта (то есть действительное имя класса, а не с точностью до наследования), мы можем использовать поле ClassInfo объекта (каждый объект в языке Delphi Pascal имеет поле ClassInfo).

if aBall.ClassInfo = BlackBall then
  ...

Хотя язык Object Pascal является языком программирования со статическими типами данных, он всегда использует динамическое связывание для того, чтобы сопоставить пересылаемое сообщение и используемый метод. Таким образом, если метод hasLiveYoung (имеет живое потомство?) определен в классе млекопитающих Mammal и переопределен для класса утконосов Platypus, поиск метода будет производиться в подклассе Platypus, даже если его получатель объявлен как класс (тип) Mammal.

Методы в языке Object Pascal связываются динамически, но законность пересылки сообщения определяется статическим классом получателя. Только если статический класс понимает пересылаемое сообщение, компилятор будет генерировать код для обработки сообщения. Чтобы проиллюстрировать это, рассмотрим пример. Пусть переменная phyl объявлена как экземпляр класса Animal, но содержит значение типа Mammal. Тогда компилятор будет возражать против сообщения hasLiveYoung, которое, по предположению, определено для млекопитающих Mammal, но не определено для класса всех животных Animal.

var
  phyl : Animal;
  newPlat : Platypus;

begin
  new (newPlat);
  phyl := newPlat;

  (*  компилятор обнаружит ошибку в этой команде  *)
  if phyl.hasLiveYoung then
    ...

10.2.2. Связывание в языке Smalltalk

Связывание в языках программирования

Smalltalk═≈ это язык с динамическими типами данных. Переменные-экземпляры, как мы видели в предыдущих главах, описываются только именем, а не типом данных. Как и во всех языках с динамическими типами данных, переопределение и обращение полиморфизма не являются проблемой, поскольку любой переменной может быть присвоено любое значение.

Можно запросить класс любого объекта. Все объекты реагируют на сообщение class, возвращая объект, представляющий класс объекта. Таким образом, если fido═≈ переменная, для которой ожидается, что она содержит значение типа Dog, то тест

( fido class == Dog ) ifTrue: [ ... ]

покажет нам, оправдались ли наши ожидания. Но тест не сработает, если мы подставим класс Mammal вместо класса Dog. Таким образом, в языке Smalltalk (как и в Objective-C) для объектов могут использоваться два теста. Первый, с именем isMemberOf:, берет имя класса в качестве аргумента и эквивалентен только что рассмотренному тесту. Второй, с именем isKindOf:, подобен функции Member в языке Object Pascal═≈ он сообщает, является ли получатель, напрямую или путем наследования, экземпляром класса, передаваемого в качестве аргумента. Таким образом, если переменная fido содержит значение типа Dog, то показанный ниже тест пройдет, а тест с использованием метода isMemberOf:═≈ нет:

( fido isKindOf: Mammal ) ifTrue: [ ... ]

Использование метода isKindOf считается плохой практикой программирования, так как он жестко связывает код со значением определенного типа. Обычно класс объекта интересует нас меньше, чем вопрос о том, будет ли он понимать определенное сообщение. Другими словами, мы больше интересуемся подтипами, чем подклассами. Таким образом, и класс тюленей Seal, и класс собак Dog могут реализовывать метод bark (лаять). Взяв определенный объект, скажем fido, мы можем использовать для определения того, ответит ли объект на сообщение bark, следующий метод:

( fido respondsTo: #bark ) ifTrue: [ ... ]

Отметим, что селектор метода представлен как символ (начинается с #).

В языке Smalltalk понятия подкласса и подтипа четко разделены. Так как статические типы недоступны, для подбора методов к сообщениям всегда используется динамическое связывание. Если получатель не понимает конкретного сообщения, выдается диагностика ошибки на этапе выполнения.

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

10.2.3. Связывание в языке Objective-C

Связывание в языках программирования

Objective-C является в своей основе языком с динамическими типами данных. Это неудивительно, учитывая мнение его создателя Бреда Кокса, приводимое в конце этой главы. Большей частью все, сказанное для языка Smalltalk о связывании и поиске методов, подходящих под пересылаемое сообщение, справедливо также и для Objective-C. Это включает и тот факт, что все объекты понимают сообщения class, isKindOf: и isMemberOf:. Когда получатель является объектом с динамическим типом данных, всегда используется динамическое связывание.

Для того чтобы определить, понимает ли данный объект конкретное сообщение, мы можем использовать системную процедуру @selector для преобразования текстового формата селектора сообщения во внутреннее представление, как это показано в следующем примере:

if ( [ fido respondsTo: @selector(bark) ] ) { ... }

Интересной особенностью языка Objective-C является возможность использовать переменные статического типа в комбинации с величинами, обладающими динамическим типом данных. В противоположность тому что было сказано в главе═3, объекты могут быть описаны с указанием статического типа, причем двумя способами. При наличии определенного класса (например, Mammal) объявление переменной

Mammal anAnimal;

создает новый идентификатор с именем anAnimal и отводит для него место в памяти. Как и в языке C++, последующие пересылки сообщений будут основываться на статическом типе данных (Mammal), и такие значения не поддерживают полиморфное присваивание. То есть если вы попытаетесь присвоить такой переменной в качестве значение экземпляр подкласса (например, Dog), то оно по-прежнему будет рассматриваться как Mammal.

Другой путь═≈ это описание, которое задается в терминах явных указателей. Оно не отводит пространство для объекта, и полиморфное присваивание в этом случае возможно. Память выделяется для таких значений с помощью сообщения new, как это делается для обычных динамических переменных. Однако когда в качестве получателя используются статические имена классов, то связывание методов, хотя оно по-прежнему является динамическим, выполняется чуть быстрее.

Mammal *fido;
fido = [ Dog new ];

Для языка Objective-C относительно легко писать контейнерные классы благодаря позднему связыванию. На самом деле большой набор таких структур данных имеется в стандартной библиотеке Objective-C.

10.2.4. Связывание в языке C++

Связывание в языках программирования

Двумя основными целями при разработке языка программирования С++ были эффективное использование памяти и скорость выполнения [Ellis═1990, Stroustrup═1994]. Он был задуман как усовершенствование языка С, в частности, для объектно-ориентированных приложений. Основной принцип С++: никакое свойство языка не должно приводить к возникновению дополнительных издержек (как по памяти, так и по скорости), если данное свойство программистом не используется. Например, если вся объектная ориентированность С++ игнорируется, то оставшаяся часть должна работать так же быстро, как и традиционный═С. Поэтому неудивительно что большинство методов в С++ связываются статически (во время компиляции), а не динамически (во время выполнения).

Как было отмечено в главе═7, язык С++ не поддерживает принцип подстановки за исключением использования указателей и значений-ссылок. Мы увидим причину этого в главе═12, где будем исследовать управление памятью в C++.

Связывание методов в этом языке является довольно сложным. Для обычных переменных (не указателей или ссылок) оно осуществляется статически. Но когда объекты обозначаются с помощью указателей или ссылок, используется динамическое связывание. В последнем случае решение о выборе метода статического или динамического типа диктуется тем, описан ли соответствующий метод с помощью ключевого слова virtual. Если он объявлен именно так, то метод поиска сообщения базируется на динамическом классе, если нет═≈ на статическом. Даже в тех случаях, когда используется динамическое связывание, законность любого запроса определяется компилятором на основе статического класса получателя.

Рассмотрим, например, следующее описание классов и глобальных переменных:

class Mammal
{
public:

    void speak()
     {
       printf("can▓t speak");
     }
};

class Dog : public Mammal
{
public:

    void speak()
     {
      printf("wouf wouf");
     }

    void bark()
     {
      printf("wouf wouf, as well");
     }
};

Mammal fred;
Dog    lassie;
Mammal *fido = new Dog;

Выражение fred.speak() печатает ╚can▓t speak╩. lassie.speak() выдаст нам собачий лай. Однако вызов fido->speak() также напечатает ╚can▓t speak╩, поскольку соответствующий метод в классе Mammal не объявлен как виртуальный. Выражение fido->bark() не допускается компилятором, даже если мы знаем, что динамический тип для fido ≈ класс Dog. Тем не менее статический тип переменной═≈ всего лишь класс Mammal, а млекопитающие обычно не лают.

Если мы добавим слово virtual:

class Mammal
{
public:

    virtual void speak()
     {
      printf("can▓t speak");
     }
};

то получим на выходе для выражения fido->speak() ожидаемый результат (а═именно, fido будет лаять).

Относительно недавнее изменение в языке С++═≈ добавление средств для распознавания динамического класса объекта. Они образуют систему RTTI (Run-Time Type Identification═≈ идентификация типа во время выполнения).

В системе RTTI каждый класс имеет связанную с ним структуру типа typeinfo, которая кодирует различную информацию о классе. Поле данных name═≈ одно из полей данных этой структуры═≈ содержит имя класса в виде текстовой строки. Функция typeid может использоваться для анализа информации о типе данных. Следовательно, следующая ниже команда будет печатать строку ╚Dog╩═≈ динамический тип данных для fido. В этом примере необходимо разыменовывать переменную-указатель fido, чтобы аргумент был значением, на которое ссылается указатель, а не самим указателем:

cout << "fido is a " << typeid(*fido).name() << endl;

Можно также спросить, используя функцию-член before, соответствует ли одна структура с информацией о типе данных подклассу класса, соотносящегося с другой структурой. Например, следующие два оператора выдают true и false:

if (typeid(*fido).before(typeid(fred))) ...
if (typeid(fred).before(typeid(lassie))) ...

До появления системы RTTI стандартный программистский трюк состоял в том, чтобы явным образом закодировать в иерархии класса методы ╚быть экземпляром╩. Например, для проверки значения переменных типа Animal на принадлежность к типу Cat или к типу Dog можно было бы определить следующую систему методов:

class Mammal
{
public:

    virtual int isaDog()
     {
      return═0;
     }

    virtual int isaCat()
     {
      return═0;
     }
};

class Dog : public Mammal
{
public:

    virtual int isaDog()
     {
      return═1;
     }
};

class Cat : public Mammal
{
public:

    virtual int isaCat()
     {
      return═1;
     }
};

Mammal *fido;

Теперь для определения того, является ли текущим значением переменной fido величина типа Dog, можно использовать команду fido->isaDog(). Если возвращается ненулевое значение, то можно привести тип переменной к нужному типу данных.

Возвращая указатель, а не целое число, мы объединяем проверку на принадлежность к подклассу и приведение типа. Это аналогично другой части системы RTTI, называемой dynamic_cast, которую мы вкратце опишем. Если некая функция в классе Mammal возвращает указатель на Dog, то класс Dog должен быть предварительно описан. Результатом присваивания является либо нулевой указатель, либо правильная ссылка на класс Dog. Итак, проверка результата все еще должна осуществляться, но мы исключаем необходимость приведения типа. Это показано в следующем примере:

class Dog;   // предварительное описание
class Cat;

class Mammal
{
public:

    virtual Dog* isaDog()
     {
      return═0;
     }

    virtual Cat* isaCat()
     {
      return═0;
     }
};

class Dog : public Mammal
{
public:

    virtual Dog* isaDog()
     {
      return this;
     }
};

class Cat : public Mammal
{
public:

    virtual Cat* isaCat()
     {
      return this;
     }
};

Mammal *fido;
Dog    *lassie;

Оператор lassie = fido->isaDog(); теперь выполним всегда. В результате переменная lassie получает ненулевое значение, только если fido имеет динамический класс Dog. Если fido не принадлежит Dog, то переменной lassie будет присвоен нулевой указатель.

lassie = fido->isaDog();

    if(lassie)
     {
      ... // fido и в самом деле относится к типу Dog
     }
    else
     {
      ... // присваивание не сработало
      ... // fido не принадлежит к типу Dog
     };

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

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

// конвертировать только в том случае, если fido является собакой
lassie = dynamic_cast < Dog* > (fido);

// затем проверить, выполнено ли приведение
if (lassie) . . .

В язык C++ были добавлены еще три типа приведения (static_cast, const_cast и reinterpret_cast), но они используются в особых случаях и поэтому здесь не описываются. Программистам рекомендуется применять их как более безопасные средства вместо прежнего механизма приведения типов.

10.2.5. Связывание в языке Java

Связывание в языках программирования

Хотя внешне язык Java похож на С++, его способы связывания сообщений и поиска подходящих методов существенно проще, чем в С++. В Java все переменные знают свой динамический тип данных. Предполагается, что подклассы являются подтипами, и поэтому значение может быть присвоено типу родительского класса без явного преобразования. Обратное присваивание (обращение полиморфизма) допускается с явным приведением типа. Для того чтобы определить допустимость присваивания, во время выполнения программы производится проверка и, если присваивание недопустимо, индуцируется исключительная ситуация.

Ball aBallValue;
  // ... пропущенный код
  // выполнить присваивание через обращение полиморфизма
BlackBall bball = (BlackBall) aBallValue;

Также можно проверять динамический тип значения, используя оператор instanceOf.

if (aBallValue instanceOf BlackBall)
  ...
else
  ...

Сообщения всегда связываются с методами на основе динамического типа данных получателя. В отличие от C++ ключевые слова virtual отсутствуют. Интересная особенность языка Java состоит в том, что поля данных также могут переопределяться, но в этом случае доступ к переменной основывается на статическом типе данных, а не на динамическом. Оба случая переопределения иллюстрируются следующим примером:

class A
{
  String name = "class A";

  public void print()
   {
    println("class A");
   }
}

class B extends A
{
  String name = "class B";

  public void print()
   {
    println("class B");
   }
}

class test
{
  public void test()
   {
   	Class B b = new B();
 	Class A a = b;

  	println(a); 	// печатает "класс А"
  	println(b); 	// печатает "класс В"

 	a.print(); 	// печатает "класс В", а не "класс А"
 	b.print(); 	// печатает "класс В"
   }
}

Хотя язык Java смешивает понятия подкласса и подтипа, предполагая, что все подклассы являются подтипами, но из всех рассматриваемых нами строго типизированных языков Java наиболее четко разделяет эти концепции, предлагая понятие интерфейса.

Интерфейсы обеспечивают иерархическую организацию, подобную классам, но не зависят от последних. Как отмечалось в главе═7, интерфейсы определяют только протокол выполнения операций, но не их реализацию. Используя ключевое слово extends, можно строить новые интерфейсы поверх существующих, как это делается для классов. Таким образом создается иерархия подтипов, полностью независимая от иерархии подклассов.

Интерфейсы могут использоваться для определения переменных, которые будут принимать значения любых типов, заявляющих, что они реализуют интерфейс. Тем самым статический тип═≈ это тип интерфейса, тогда как динамический тип═≈ это тип класса. Связывание основывается на динамическом типе.

Например, при проектировании на языке Java системы итератора для структур данных (см. главу═16, где обсуждаются итераторы) можно представить себе иерархию итераторов: однонаправленные, двунаправленные, с произвольным доступом. Они определены следующими протоколами:

interface forwardIterator
{
  void advance();
  int currentValue();
}

interface bidirectionalIterator extends forwardIterator
{
  void moveBack();
}

interface randomAccessIterator extends
  bidirectionalIterator
{
  int at(int);
}

Структуры данных могут затем определить нужный итератор. Это задается реализацией.

class listIterator implements forwardIterator
{
  // ...
}

10.3. Как связывать: статически или динамически?

Разделы

Споры по поводу относительных достоинств статических и динамических типов данных, а также статического и динамического связывания неизменно сводятся к противопоставлению эффективности и гибкости. Формулируя вкратце, статические типы данных и статическое связывание более эффективны, а динамические типы данных и динамическое связывание более гибки.

Как мы видели, динамические типы данных подразумевают, что каждый объект должен отслеживать свой собственный тип данных. Если мы посмотрим на объекты с точки зрения ╚непреклонного индивидуалиста╩ (каждый объект должен сам заботиться о себе и не зависеть от других), то динамические типы данных кажутся более объектно-ориентированной методикой. Их применение сильно упрощает, например, разработку структур данных общего назначения. Но не все так просто═≈ во время выполнения происходит постоянный поиск подходящего кода. Хотя имеются способы уменьшения этих затрат, они не могут быть устранены полностью. В основном именно потому, что такие накладные расходы могут быть весьма значительными, большинство языков программирования использует статические типы данных.

Статические типы данных упрощают реализацию языка, даже если (как в Java, Object Pascal и иногда в C++) используется динамическое связывание метода с сообщением. Когда компилятору известны статические типы данных, выделение памяти под переменные происходит более эффективно, а для простых операций (например, сложение чисел) генерируется почти совершенный код. Но имеются затраты, связанные и со статическими типами данных. К примеру, часто объекты могут терять знание о себе (хотя это не обязательно, как мы видели в случае языка Object Pascal). Как мы отмечали при обсуждении проблемы контейнеров данных, этот факт затрудняет написание абстракций структур данных общего назначения.

Если статические типы данных упрощают реализацию языка программирования, то статическое связывание делает его почти элементарным. Если соответствие между методом и сообщением может быть обнаружено компилятором, то пересылка сообщений (независимо от используемого синтаксиса) представляется просто как вызов процедуры: во время выполнения уже не требуется поиск подходящего метода. Это, по-видимому, максимально эффективно (хотя механизм inline-функций в C++ может устранить даже и код вызова процедуры).

Напротив, динамическое связывание всегда требует некоего run-time-механизма (хотя бы и примитивного) для сопоставления метода и сообщения. В языках программирования, которые используют динамические типы данных (и, как следствие, динамическое связывание), вообще говоря, нельзя определить заранее, будет ли пересылаемое сообщение восприниматься получателем. Когда сообщение не распознается, то не остается другой альтернативы, кроме как сгенерировать сообщение об ошибке этапа выполнения. Защитники статических языков программирования настаивают, что большинство таких ошибок может быть отловлено на этапе компиляции для языка со статическим связыванием методов. Эта точка зрения оспаривается (как и следовало ожидать) сторонниками динамического программирования.

Тем самым нам приходится решать, что более важно: эффективность или гибкость, правильность или легкость использования. Брэд Кокс [Cox═1986] настаивает, что решение зависит как от уровня абстракции программы, так и от того, являемся ли мы производителями или потребителями программной системы. Кокс утверждает, что объектно-ориентированное программирование будет основным (хотя и не единственным) средством в "революции в области программной индустрии". Точно так же как в девятнадцатом веке индустриальная революция стала возможна только после разработки взаимозаменяемых деталей, так и целью программной революции является конструирование многократно используемых и надежных абстрактных компонент высокого уровня для программного обеспечения, из которых собираются реальные приложения.

Эффективность═≈ главное, о чем следует думать на низком уровне (Кокс называет этот уровень "абстракцией уровня транзисторов" ("gate-level abstractions") по аналогии с электроникой). По мере повышения уровня абстракции (до интегральных микросхем и готовых плат) гибкость становится более существенной.

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

Итак, нет единственно правильного ответа на вопрос, какая из техник связывания является более подходящей. Все зависит от конкретной ситуации.

Упражнения

Разделы