Глава     4

Сообщения, экземпляры и инициализация


Разделы

Содержание

В главе═3 мы вкратце охарактеризовали объектно-ориентированное программирование в статике. А именно мы описали, как создаются новые типы, классы и методы. В этой главе мы продолжим исследование механизмов ООП, изучив динамические свойства. Они определяют то, как создаются новые переменные, как они инициализируются и взаимодействуют друг с другом путем пересылки сообщений.

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

4.1. Синтаксис пересылки сообщений

Разделы

Мы используем термин пересылка сообщений (иногда говорят о поиске методов) для обозначения динамического процесса обращения к объекту с требованием выполнить определенное действие. В главе═1 мы неформально описали пересылку сообщений и отметили, чем она отличается от обычного вызова процедуры. В частности:

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

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

4.1.1. Синтаксис пересылки сообщений в Object Pascal

Синтаксис пересылки сообщений

В отличие от остального ООП-сообщества программисты на языке Delphi Pascal используют термин сообщение для специфической команды управления окном. Более традиционное его истолкование известно среди них как поиск метода.

Поиск метода в языке Object Pascal═≈ это просто запрос, посылаемый объекту, чтобы вызвать один из его методов. Как мы заметили в главе═3, метод описывается при определении объекта так же, как поле данных в записи. Аналогично, стандартная синтаксическая конструкция с использованием точки, которая применяется для описания поля данных, расширена и обозначает также вызов метода. Селектор сообщения═≈ то есть текст, следующий за точкой,═≈ должен соответствовать одному из методов, определенных для класса или наследуемых от родительского класса (мы изучаем наследование в главе═7). Тем самым если идентификатор aCard описан как объект класса Card, то следующая команда приказывает игральной карте нарисовать себя в указанном окне в нужной точке.

aCard.draw(win,═25,═37);

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

aCard.flip;

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

Компилятор проверяет корректность пересылки сообщений. Попытка переслать сообщение, которое не указано в описании класса объекта-получателя, приводит к ошибке. Внутри метода псевдопеременная self используется для ссылки на получателя сообщения. Эта переменная не описывается, но может быть использована как аргумент или как получатель других сообщений.

Например, метод color, который возвращает цвет игральной карты, может быть записан следующим образом:

function Card.color : colors;
var ss : suits;

begin

     ss := self.suit;
     if (ss = Heart) or (ss = Diamond) then
         color:=Red
     else
         color:=Black;
end;

Здесь метод suit вызывается с целью получить значение масти. Это считается более удачной стратегией программирования, чем прямой доступ к полю данных suitValue. Delphi Pascal также позволяет возвращать результат функции, присваивая его специальной переменной Result, а не идентификатору функции (color в нашем случае).

4.1.2. Синтаксис пересылки сообщений в C++

Синтаксис пересылки сообщений

Как мы отметили в главе═3, несмотря на то что концепции методов и сообщений применимы к языку С++, собственно методы и сообщения (как термины) редко используются в текстах по C++. Метод принято называть функцией═≈ членом класса (member function); о пересылке сообщений говорят как о вызове функции-члена.

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

Если theCard описан как экземпляр класса Card, то следующий оператор приказывает игральной карте отобразить себя в заданном окне в точке с координатами═25 и═37:

theCard.draw(win,═25,═37);

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

if ( theCard.faceUp() )
{
      ...
}
else
{
      ...
}

Как и в языке Object Pascal, компилятор проверяет правильность пересылки сообщения, обращаясь к описанию класса получателя. Попытка передать сообщение, которое не является частью класса, отмечается как ошибка при компиляции.

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

С каждым методом ассоциирована псевдопеременная, в которой указан получатель сообщения. В языке C++ она называется this и является указателем на получателя, а не собственно получателем. Поэтому используется разыменование указателя (операция ->) для пересылки последующих сообщений к тому же получателю. Например, метод color, который используется для определения цвета карты, может быть записан следующим образом (если мы хотим избежать прямого доступа к внутреннему полю, содержащему значение масти):

colors Card::color()
{
    switch ( this->suit() )
     {
      case heart:
      case diamond:
       return red;
     }
    return black;
}

Переменная this также часто используется, если внутри метода надо передать получателя сообщения в качестве аргумента другой функции:

void aClass::aMessage(bClass b, int x)
{
    // передать самого себя в качестве аргумента
    b->doSomething(this, x);
}
В языке C++ можно опускать использование this в качестве получателя. Внутри тела метода вызов другого метода без явного указания получателя интерпретируется как сообщение для текущего получателя. Тем самым метод color может быть (и обычно будет) записан следующим образом:

colors Card::color()
{
    switch ( suit() )
     {
      case heart:
      case diamond:
       return red;
     }
    return black;
}

4.1.3. Синтаксис пересылки сообщений в Java

Синтаксис пересылки сообщений

Синтаксис для пересылки сообщений в языке Java почти идентичен используемому в C++. Единственное заметное отличие состоит в том, что псевдопеременная this в языке C++ обозначает указатель на объект, а в языке Java является собственно объектом (поскольку в языке Java нет указателей!).

4.1.4. Синтаксис пересылки сообщений в Smalltalk

Синтаксис пересылки сообщений

Синтаксис языка Smalltalk отличен от того, который используется в языках C++ или Object Pascal при пересылке сообщений. По-прежнему первая часть выражения описывает получателя═≈ объект, которому предназначается сообщение. В качестве разделителя применяется пробел, а не точка.

Как и в языке C++, селектор должен соответствовать одному из методов, определенных для класса получателя. Однако в отличие от C++ Smalltalk является языком программирования с динамическими типами данных. Это означает: проверка того что класс получателя понимает селектор сообщения, выполняется во время работы программы, а не на этапе компиляции.

Если идентификатор aCard═≈ это переменная класса Card, то следующий оператор приказывает перевернуться соответствующей карте:

aCard flip

Как мы отметили в главе═3, аргументы метода выделяются ключевыми селекторами. За каждым ключевым словом-селектором следует двоеточие, а затем═≈ аргумент. Следующее выражение приказывает переменной aCard нарисовать себя в окне в точке с координатами═25 и═37:

aCard drawOn: win at:═25 and:═37

В языке Smalltalk даже бинарные операторы, вроде + или *, интерпретируются как сообщения, причем получателем является левый член выражения, а правый передается как аргумент 4 . Как и для ключевых слов или унарных сообщений (сообщений без аргумента), классы вправе наделить бинарные сообщения каким угодно смыслом. Есть приоритеты: унарные сообщения имеют наивысший приоритет, затем следуют бинарные сообщения и наконец ключевые слова.

Комментарий
4 Язык C++ обеспечивает аналогичную перегрузку бинарных операторов (таких, как +, -, < или оператор присваивания =). Эта тема выходит за рамки данной книги, хотя мы и упомянем о перегрузке оператора присваивания в главе 12.

В языке Smalltalk псевдопеременная self внутри метода обозначает получателя сообщения. Это значение часто используется как получатель других сообщений, что подразумевает, что получатель желает послать сообщение самому себе. Например, описанный ранее метод color может быть переписан следующим образом, чтобы избежать прямого обращения к переменной экземпляра suit:

color
   " вернуть цвет карты "
   (self suit = #diamond)    ifTrue: [ ^ #red ].
   (self suit = #club)       ifTrue: [ ^ #black ].
   (self suit = #spade)      ifTrue: [ ^ #black ].
   (self suit = #heart)      ifTrue: [ ^ #red ].

4.1.5. Синтаксис пересылки сообщений в языке Objective-C

Синтаксис пересылки сообщений

Синтаксис и терминология пересылки сообщений в языке Objective-C напоминает Smalltalk. Имеются ключевые слова и унарные сообщения, но в отличие от языка Smalltalk классы в Objective-C не могут переопределять бинарные операторы.

Пересылка сообщений в языке Objective-C осуществляется только внутри вызовов сообщений. Такие вызовы═≈ это выражения, заключенные в квадратные скобки [ ]. Например, если идентификатор aCard представляет собой экземпляр класса Card, то следующее выражение приказывает карте перевернуться (обратите внимание на точку с запятой в конце):

[ aCard flip ];

Подобно языку Smalltalk, Objective-C использует динамические типы данных. Это означает: проверка того, что получатель способен понять сообщение, выполняется во время работы программы, а не на этапе компиляции. Если получатель не распознал сообщения, генерируется сообщение об ошибке.

Синтаксис языка Smalltalk разрешает использовать сообщения с аргументами. Например, следующая команда приказывает карте aCard нарисовать себя в заданном окне в точке с координатами═25 и═36:

[ aCard drawOn: win at:═25 and:═36 ];

Квадратные скобки нужны только при вызове сообщения. Если пересылка сообщения требует возвращаемого значения, то оператор присваивания должен располагаться за пределами квадратных скобок. Так, нижеследующее выражение присваивает переменной newCard копию карты aCard:

newCard = [ aCard copy ];

Пересылка сообщения может использоваться везде, где разрешены обычные выражения языка C. Например, в следующем операторе языка C проверяется, лежит ли карта aCard картинкой вверх:

if ( [ aCard faceUp ] ) ...

Подобно языку Object Pascal, внутри методов Objective-C используется идентификатор self для ссылки на получателя сообщения. Однако в отличие от других языков self является истинной переменной, которая может быть модифицирована пользователем. Мы увидим в разделе═4.3.3, посвященном ╚методам-фабрикам╩, что такая модификация бывает полезна.

4.2. Способы создания и инициализации

Разделы

Перед тем как детально рассмотреть механизм создания и инициализации в каждом из изучаемых языков программирования, мы рассмотрим некоторые сопутствующие моменты. Зная о различных способах, читатель будет лучше подготовлен к восприятию свойств, которые присущи (или не присущи) тому или иному языку программирования. Мы будем рассматривать выделение памяти (через стек или через ╚кучу╩ (heap)), освобождение памяти, работу с указателями и создание объектов с неизменяемым состоянием.

4.2.1. Стек против ╚кучи╩

Способы создания и инициализации

Вопрос о выделении памяти через стек или через ╚кучу╩═ связан с тем, как выделяется и освобождается память под переменные и какие явные шаги должны при этом предприниматься программистом. Различают автоматические и динамические переменные. Существенная разница между ними состоит в том, что память для автоматических переменных создается при входе в процедуру или блок, управляющий этими переменными. При выходе из блока память (опять же автоматически) освобождается. Многие языки программирования используют термин статический (static) для обозначения переменных, автоматически размещаемых в стеке. Мы будем придерживаться термина автоматический, потому что он более содержателен и потому что static═≈ это ключевое слово, которое обозначает нечто другое в языках C и C++. В момент, когда создаются автоматические переменные, происходит связывание имени и определенного участка памяти, и эта связь не может быть изменена во время существования переменной.

Рассмотрим теперь динамические переменные. Во многих традиционных языках программирования (Pascal) динамические переменные создаются системной процедурой new(x), которая использует в качестве аргумента переменную-указатель. Пример:

type

    shape: record
    form : (triangle, square);
    side : integer;

  end;

var

  aShape : ╜ shape;

begin

  new (aShape);
  ...

end.

Здесь выделяется новый участок памяти, и в качестве побочного эффекта значение переменной-аргумента изменяется так, что теперь она указывает на новый участок памяти. Тем самым процессы выделения памяти и привязки к имени взаимосвязаны.

В других языках (таких, как C) выделение памяти и привязка к имени не связаны друг с другом. Например, в C выделение памяти осуществляется системной библиотечной функцией malloc, которой в качестве аргумента передается объем выделяемой памяти. Функция malloc возвращает адрес блока памяти, который затем присваивается переменной-указателю с помощью обычного оператора присваивания. Следующий пример иллюстрирует этот процесс:

struct shape
  {
    enum {triangle, square} form;
    int side;
  };

shape *aShape;
...
aShape = (struct shape *)

    malloc(sizeof(struct shape));
...

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

4.2.2. Восстановление памяти

Способы создания и инициализации

Когда используется выделение памяти через ╚кучу╩, необходимо обеспечить специальные средства для возврата памяти, которая больше не нужна. В общем случае языки программирования разбиваются на две большие категории. Паскаль, C и C++ требуют, чтобы пользователь сам отслеживал, какие данные ему больше не нужны, и в явном виде освобождал эту память с помощью подпрограмм из системной библиотеки. К примеру, в языке Паскаль такая подпрограмма называется dispose, а в языке C═≈ free.

Другие языки (Java, Smalltalk) могут автоматически отследить ситуацию, когда к объекту больше нет доступа (тем самым он изолирован от последующих вычислений). Такие объекты автоматически собираются и уничтожаются, а выделенная для них память помечается как свободная. Этот процесс называется сборкой мусора. Для такого восстановления памяти имеется несколько хорошо известных алгоритмов. Их описание выходит за пределы данной книги. Хороший обзор алгоритмов сборки мусора дается Коэном [Cohen═1981].

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

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

4.2.3. Указатели

Способы создания и инициализации

Указатели═≈ эффективное средство работы с динамической информацией, и по этой причине они используются практически во всех объектно-ориентированных языках. Вопрос: ╚Указатели═≈ это полезная абстракция или нет?╩ активно дискутируется в сообществе программистов. Это нашло свое отражение в том факте, что не все объектно-ориентированные языки поддерживают указатели.

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

4.2.4. Создание неизменяемого экземпляра объекта

Способы создания и инициализации

В главе═3 мы описали класс Card и отметили одно свойство, желательное для абстракции игральных карт,═≈ а именно, чтобы значения масти и ранга (достоинства) карты задавались бы лишь однажды и больше не менялись. Переменные, подобные полям данных suit и rank (они не меняют своих значений во время выполнения программы), называются переменными с однократным присваиванием (single-assignment variables) или же неизменяемыми переменными (immutable variables). Объект, у которого все переменные экземпляра являются неизменяемыми, в свою очередь называется неизменяемым объектом.

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

Конечно, неизменяемые объекты могут конструироваться просто по соглашению. Например, мы можем предусмотреть сообщение, задающее масть и ранг игральной карты, и затем просто полагаться на то, что пользователь будет использовать это средство лишь однажды при создании объекта, а не для последующего изменения состояния объекта. Более предусмотрительные объектно-ориентированные разработчики предпочитают не полагаться на добрую волю своих потребителей и используют механизмы языка, которые гарантируют то, что нужно. Языки программирования, которые мы рассматриваем, отличаются по степени, в какой они обеспечивают подобную услугу. 4.3. Механизмы создания и инициализации

Разделы

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

4.3.1. Создание и инициализация в C++

Механизмы создания и инициализации

Язык C++ следует C (а также Pascal и другим алголоподобным языкам), поддерживая и автоматические, и динамические переменные. Автоматической переменной память назначается при входе в блок, в котором находится ее объявление, а при передаче управления за пределы блока память освобождается. Одно изменение по сравнению с языком C: объявление не обязательно размещается в начале блока. Единственное требование состоит в том, что объявление должно появляться до первого использования переменной. Тем самым оно может передвигаться непосредственно к точке, в которой используется переменная.

Неявная инициализация обеспечивается в языке C++ за счет использования конструкторов. Как было отмечено в главе═3, конструктор═≈ это метод, который имеет то же самое имя, что и класс объекта. Хотя определение конструктора═≈ это часть определения класса, на самом деле конструктор задействуется в процессе инициализации. Поэтому мы обсуждаем конструкторы здесь. В частности, метод-конструктор автоматически и неявно вызывается каждый раз, когда создается объект, принадлежащий к соответствующему классу. Обычно это происходит при объявлении переменной, но также и в том случае, когда объект создается с помощью оператора new или когда по каким-то причинам применяются временные переменные.

Например, рассмотрим следующее описание класса, которое может быть использовано как часть абстрактного типа данных ╚комплексное число╩:

class Complex
{
public:

        // различные конструкторы
        Complex();
        Complex(double);
        Complex(double,double);

        // операции над числами
        ...
private:

        // поля данных
        double realPart;
        double imaginaryPart;
};

Здесь задаются три конструктора класса. Какой конструктор будет вызван, зависит от аргументов, используемых при создании переменной. Объявление без аргументов

Complex numberOne;

приводит к вызову первого конструктора. Его тело может выглядеть следующим образом:

Complex::Complex()
{
    // инициализировать поля нулями
    realPart=0.0;
    imaginaryPart=0.0;
}

Заметьте, что тело конструктора отличается от нормального метода еще и тем, что отсутствует явный тип возвращаемого результата. Механизм конструкторов делается значительно более мощным, когда он комбинируется со способностью языка C++ перегружать имена функций (функция называется перегруженной, если имеются две или более функции с одним именем). В языке C++ двусмысленность при вызове перегруженных функций разрешается за счет различий в списке аргументов.

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

Complex pi =	3.14159265359;
Complex e  	(2.7182818285);
Complex i  	(0.0,═1.0);

Первые две инициализации вызывают конструктор, который имеет только один аргумент. Описание такого конструктора имеет вид:

Complex::Complex(double rp)
{
    // задать вещественную часть
    realPart = rp;

    // задать нулевую мнимую часть
    imaginaryPart =═0.0;
}

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

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

Complex::Complex(double rp) :
         realPart(rp), imaginaryPart(0.0)

{ /* дальнейшая инициализация просто не нужна */ }

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

Complex *c;

c = new Complex(3.14159265359, -1.0);

Круглые скобки не являются обязательными, если никакие аргументы не передаются:

Complex *d = new Complex;

Массив значений может быть создан, если указать его размер в квадратных скобках. Он вычисляется во время работы программы и может быть произвольным целочисленным выражением. Значения, созданные таким способом, инициализируются с помощью конструктора ╚по умолчанию╩ (то есть конструктора, не имеющего аргументов):

Complex *carray = new Complex[27];

Память для динамически размещенных значений должна быть явным образом освобождена программистом с помощью оператора delete (или delete[] в случае массива объектов).

Значения в языке C++ могут быть объявлены неизменяемыми с помощью ключевого слова const. Такие данные становятся константами═≈ их не разрешается изменять. Переменные экземпляра, описанные как константы, обслуживаются инициализаторами, поскольку константа не может встречаться в левой части оператора присваивания.

В языке C++ могут определяться функции, которые автоматически вызываются при освобождении памяти, выделенной под объект. Они называются деструкторами. Для автоматических переменных память освобождается при выходе из процедуры, в которой описана переменная. Для динамически размещаемых переменных память возвращается с помощью оператора delete. Функция-деструктор получает имя класса с предшествующим знаком ╚тильда╩ (~). Она не имеет аргументов и редко вызывается в явном виде.

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

class Trace
{
public:

        // конструктор и деструктор
        Trace(*char);
        ~Trace();

private:

        char *text;
};

Trace::Trace(char *t) : text(t)
{
    printf("entering %s\n", text);
}

Trace::~Trace()
{
    printf("exiting %s\n", text);
}

Для отладки программы разработчик всего лишь создает в каждой процедуре, подлежащей трассировке, описание ╚пустой╩ переменной типа Trace с соответствующей строкой инициализации. Рассмотрим следующую пару подпрограмм:

void procedureA()
{
    Trace ("procedure A");
    procedureB(7);
}

void procedureB(int x)
{
    Trace ("procedure B");
    if (x <═5)
     {
      Trace ("Small case in procedure B");
      ...
     }
    else
     {
      Trace ("Large case in procedure B");
      ...
     };
    ...
}

В выходном листинге объекты типа Trace покажут порядок выполнения программы. Типичный выходной листинг может выглядеть как:

entering procedure A
entering procedure B
entering Large case in procedure B
...
exiting Large case in procedure B
exiting procedure B
exiting procedure A

4.3.2. Создание и инициализация в языке Java

Механизмы создания и инициализации

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

Все переменные типа ╚объект╩ в языке Java первоначально получают значение null. Объекты создаются оператором new. В отличие от C++ в языке Java круглые скобки должны использоваться в операторе даже тогда, когда не требуется никаких аргументов:

// создать пятерку пик
Card aCard = new Card (Card.spade, 5);

// создать карту по умолчанию
Card bCard = new Card ();

Понятие конструктора в языке Java аналогично C++ с тем исключением, что конструкторы в Java не поддерживают инициализаторов. В отличие от C++ конструктор в языке Java может вызывать другие конструкторы того же объекта, что зачастую позволяет исключить из нескольких конструкторов общие операторы. Конструктор при этом должен вызываться с помощью ключевого слова this:

class NewClass
{   
    NewClass(int i)
     {
       // выполнить инициализацию═1-го типа
       ...
     }

    NewClass(int i, int j)
     {
       // прежде всего вызвать первый конструктор
       this(i);
       // продолжить инициализацию
       ...
     }
}

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

Необычным свойством языка Java является использование текстовой строки как аргумента оператора new для определения в процессе работы программы типа объекта, который должен быть размещен. В более общем случае строка является выражением, которое строится во время выполнения. Для инициализации вновь создаваемого объекта будет использован конструктор без аргументов.

// построить экземпляр класса Complex
a = new ("Comp" + "lex");

Как мы видели в главе═3, переменные, которым нельзя повторно присвоить значение, создаются с ключевым словом final. Однако они не являются по-настоящему неизменными, поскольку ничто не препятствует программисту переслать такому объекту сообщение, которое повлияет на его внутреннее состояние. Тем самым значения типа final не эквивалентны значениям типа const в языке C++, которые являются гарантированно неизменными.

final aCard = new Card(Card.spade, 3);
    aCard.flip(); // изменить состояние карты

4.3.3. Создание и инициализация в Objective-C

Механизмы создания и инициализации

Язык Objective-C комбинирует синтаксис языка Smalltalk и описаний языка C. Обычно объекты описываются как экземпляры типа id, так как более подробная информация о типе данных может быть неизвестна вплоть до выполнения программы. Фактическое размещение объекта выполняется (как и в языке Smalltalk) пересылкой сообщения new объекту класса. (Заметьте, что id определен через команду typedef и что на самом деле переменная типа id═≈ это указатель на собственно объект.)

Переменные, описываемые как id, всегда являются динамическими. Как мы увидим в главе═7, в языке Objective-C разрешается также описывать переменные с использованием явного имени класса. Такие переменные являются автоматическими, и память под них выделяется из стека при входе в процедуру, внутри которой они описаны. Соответственно эта память освобождается при выходе из процедуры.

Вариации процесса инициализации в языке Objective-C обеспечиваются фабричными методами. Подобно методам класса в языке Smalltalk фабричные методы определяют функциональность конкретного класса. (Этим они отличаются от обычных методов, которые определяют функциональность экземпляра класса. Один из наиболее сложных аспектов в объектно-ориентированном программировании═≈ различение атрибутов экземпляров и классов. Мы вернемся к этому вопросу в следующих главах.)

Если определению метода предшествует символ ╚+╩ (плюс), то это фабричный метод. Метод, перед которым стоит знак ╚-╩ (минус),═≈ это экземплярный метод. Следующий пример иллюстрирует определение фабричного метода, который создает и инициализирует экземпляры класса Card.

@implementation Card
{
+ suit: (int) s rank: (int) r
  {
      self = [Card new ];
      suit = s; rank = r;

      return self;
  }
}
@end

Заметьте, что сообщение new используется для того, чтобы создать новый экземпляр класса, как и в языке Smalltalk. Компилятор Objective-C не выдает предупреждающих сообщений, если переменные экземпляра используются внутри фабричных методов. Если встречаются такие ссылки, то предполагается (хотя это предположение и не проверяется), что значение переменной self относится к ╚правильному╩ экземпляру класса. Поскольку обычно внутри фабричных методов идентификатор self ссылается собственно на класс, а не на экземпляр класса, то пользователь должен первым делом изменить значение self перед доступом к полям экземпляра. (Тот факт, что тип переменной self не проверяется на правильность перед тем, как используются переменные экземпляра, может быть источником трудно уловимых ошибок в программах на языке Objective-C.) В предшествующем методе только после того, как переменная self была изменена, ссылки на поля экземпляра suit и rank стали относиться к конкретным ячейкам.

Хотя объекты размещаются в языке Objective-C динамически, система не осуществляет автоматическое управление памятью. Пользователь с помощью сообщения free должен предупредить систему, что выделенная объекту память больше не используется. Сообщение free определено для класса Object, и поэтому распознается всеми объектами:

[ aCard free ];

Язык Objective-C не обеспечивает прямого способа задания неизменяемых или постоянных значений.

4.3.4. Создание и инициализация в Object Pascal

Механизмы создания и инициализации

В языке Object Pascal все объекты являются динамическими и должны создаваться в явном виде с помощью системной функции new. Ее аргументом является идентификатор объекта. Аналогично для освобождения памяти, занимаемой объектом, используется системная подпрограмма dispose, которая вызывается, когда объект больше не нужен. Управление памятью посредством подпрограмм new и dispose═≈ обязанность пользователя. Язык программирования обеспечивает только минимальную поддержку. Если нет достаточной памяти для обеспечения запроса на размещение объекта, то возникает ошибка выполнения. Если делается попытка использовать объект, который не был размещен надлежащим способом, то ошибка выполнения иногда возникает, а иногда═≈ нет.

type

    Complex : object
        rp : real;
        ip : real;
        procedure initial (r, i : real);
        ...
    end;

var

    aNumber : Complex;

procedure Complex.initial(r, i : real);

begin
     rp:=r; ip:=i;
end;

begin
     new(aNumber);
     aNumber.initial(3.14159265359,═2.4);
     ...
     dispose(aNumber);
end.

Заметьте, что при работе с объектами не требуется оператор разыменования, несмотря на родство объектов и указателей. Другими словами, ссылки на объекты═≈ не то же самое, что явные указатели на значения (хотя объекты и являются динамическими переменными).

Язык Object Pascal фирмы Apple отличается от других объектно-ориентированных языков тем, что не поддерживает неявной инициализации объектов. Как только объект создан (через функцию new), обычно явным образом вызывается инициализирующий метод. Это иллюстрирует приведенный выше пример.

Поддержка защиты данных в языке Apple Object Pascal является слабой. Например, нет способа предотвратить явный доступ к полям rp и ip экземпляра класса Complex, а также нет гарантии того, что сообщение initial не вызывается более одного раза.

Язык Object Pascal в версии Delphi ближе к C++. Мы видели, как поля в определении класса могут быть спрятаны от пользователя директивой private. Язык Delphi Pascal также поддерживает конструкторы. Как мы видели в разделе═3.5.1, они описываются в определении класса с помощью ключевого слова constructor. Затем объекты создаются путем использования метода-конструктора в виде сообщения, посылаемого собственно классу:

aCard := Card.Create(spade, 5);

С помощью вызова функции-конструктора выделяется память под новое значение, которое будет автоматически проинициализировано. В отличие от языка C++ Delphi Pascal не разрешает перегружать функции с одинаковыми именами, но зато позволяет использовать несколько различных конструкторов.

Язык Delphi Pascal также поддерживает деструкторы. Функция-деструктор (обычно называемая Destroy) описывается с помощью ключевого слова destructor. Когда освобождается динамически размещенный объект, метод free проверяет идентификатор self на совпадение с nil и для непустого объекта вызывает функцию-деструктор.

type

    TClass = class (TObject)
        constructor Create(arg : integer);
        procedure doTask(value : integer);
        destructor Destroy;
    end;

destructor TClass.Destroy;

begin
   (* некоторые действия *)
end;

Хотя язык Delphi Pascal не поддерживает неизменяемые или постоянные поля данных в явном виде, такие значения могут быть смоделированы за счет конструкции, называемой property (свойство). Поле property описывается и обрабатывается подобно полям данным (доступ к значению осуществляется по имени, а запись═≈ через команду присваивания). Однако синтаксис скрывает истинный механизм доступа. Наиболее часто и присваивание, и доступ к имени осуществляются через специальную функцию, хотя иногда поле property используется просто как псевдоним для другого поля данных. Пример полей property приведен ниже. Если они содержат лишь ключевую команду read, то имеют статус ╚только для чтения╩, а если ключевую команду write═≈ ╚только для записи╩. (Заметьте, что значениям ╚только для чтения╩ разрешается присваивать что-либо внутри конструктора или других методов, имеющих доступ к области private класса.)

type
    TnewClass = class (TObject)
        property readOnly  : Integer
                             read internalValue;
        property realValue : Real
                             read internalReal
                             write checkArgument;
    private
        internalValue : Integer;
        internalReal  : Real;
        procedure CheckArgument (arg : Real);
    end;

4.3.5. Создание и инициализация в языке Smalltalk

Механизмы создания и инициализации

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

Значение, возвращаемое методом new объекта-класса, существует само по себе, хотя обычно оно немедленно присваивается идентификатору через оператор присваивания, либо же передается в качестве аргумента. Например, следующая команда создает новый экземпляр нашего класса Card:

aCard := Card new.

Пользователь Smalltalk не предпринимает явных действий для освобождения памяти. Значения, к которым больше нет доступа, автоматически освобождаются системой. Если даже после сборки мусора оказывается невозможным удовлетворить запрос на выделение памяти, то возникает ошибка выполнения.

Язык Smalltalk обеспечивает несколько механизмов инициализации объекта. Класс-методы═≈ это методы, ассоциированные с конкретным класс-объектом. Мы будем рассматривать класс-объекты и класс-методы более подробно в главе═18. Сейчас достаточно знать, что класс-методы могут быть использованы только как сообщения для класс-объектов. Тем самым они заменяют сообщение new. Зачастую эти методы вызывают new для создания нового объекта, а затем выполняют дальнейшую инициализацию. Поскольку класс-методы не могут напрямую обращаться к полям объекта, они посылают сообщения экземпляру, чтобы выполнить инициализацию.

Ниже приведен класс-метод suit:rank:. Он создает новый экземпляр (объект) класса Card и затем вызывает метод suit:rank: (предположительно определенный как класс-метод класса Card) для присваивания значений переменным экземпляра.

suit: s rank: r ╫ newCard ╫
      " сперва создаем карту "
      newCard := Card new.

      " затем инициализируем ее "
      newCard suit: s rank: r.

      " наконец, возвращаем ее "
      ^  newCard

Чтобы создать новый экземпляр класса Card═≈ например, четверку бубей,═≈ пользователь вводит следующую команду:

aCard := Card suit: #diamond rank:═4.

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

Язык Smalltalk не обеспечивает прямого механизма инициализации неизменяемых полей. Часто методы, подобные suit:rank:, имеют пометку private. Это предполагает, что такие методы не должны вызываться пользователями напрямую. Однако такое ограничение выполняется только в силу соглашения, а не вынуждается собственно языком программирования.

Другая техника инициализации объектов в языке Smalltalk═≈ это каскад сообщений. Его применяют, если несколько сообщений должны быть посланы одному и тому же получателю (как это часто бывает при инициализации). Для каскадирования сообщений вслед за получателем записывают список сообщений, разделяемых точками с запятой. Например, следующее сообщение создает новое множество Set и инициализирует его значениями═1,═2,═3. Результат, который присваивается переменной aSet,═≈ это новое проинициализированное множество. Использование каскадов часто позволяет отказаться от временных переменных:

aSet := Set new add:═1; add:═2; add:═3.

Упражнения

Разделы

  1. Напишите метод copy для класса Card из главы═3. Метод должен возвращать новый экземпляр класса Card с полями масти и ранга, инициализированными такими же значениями, что и у получателя сообщения copy.
  2. Как бы вы разработали программное средство обнаружения несанкционированного доступа для языка программирования, не обеспечивающего прямой поддержки неизменяемых переменных экземпляра? (Подсказка: спрячьте соответствующие директивы в комментарии. Программное средство должно будет их анализировать и использовать.)
  3. Мы видели два стиля вызова методов. Подход, используемый в C++, аналогичен традиционному вызову функции. Стиль, принятый в языках Smalltalk и Objective-C, разделяет аргументы ключевыми словами. Что по вашему мнению читается легче? Что более информативно? Какой подход лучше защищен от ошибок? Обоснуйте вашу точку зрения.
  4. Как бы вы разработали программное средство для детектирования описанных в разделе═4.2.2 проблем, связанных с выделением и освобождением памяти?
  5. А.═Аппель [Appel═1987] настаивает, что при определенных обстоятельствах выделение памяти из ╚кучи╩ может быть более эффективным, чем использование стека. Прочтите его статью и суммируйте ключевые положения Аппеля. Насколько вероятно, что ситуации, для которых это утверждение справедливо, встретятся на практике?
  6. Напишите короткое (два-три абзаца) эссе за или против автоматической ╚сборки мусора╩.

Сообщения, экземпляры и инициализация