Глава     20

Новый взгляд на классы


Разделы

Содержание

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

Что же в точности представляет собой класс? Ответ на этот вопрос зависит, в частности, от того, какой язык программирования вы рассматриваете. В широком смысле есть две основные школы. Одни языки, такие как C++ или Object Pascal, рассматривают класс как тип данных, подобный целым числам или записям. Другие языки, такие как Smalltalk или Objective-C, считают, что класс — это объект. В следующих двух разделах мы рассмотрим некоторые вариации этих двух основных точек зрения.

20.1. Классы как типы

Разделы

Чтобы понять смысл высказывания «классы суть типы», мы прежде всего должны попытаться уяснить значение термина тип в языках программирования. К сожалению, понятие типа данных используется для очень многих целей. Следующий список, взятый из [Wegner 1986], иллюстрирует некоторые ответы на вопрос «Что такое тип данных?»:

Приведенный выше список не претендует на полноту. Однако с него можно начать обсуждение классов как типов данных.

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

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

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

20.1.1. Как наследование усложняет понятие типа

Классы как типы

Переопределенный метод не просто заменяет поле в записи, но, скорее, изменяет поведение объекта, причем произвольным образом. Давайте рассмотрим влияние такой замены на процесс верификации 15 . Программист разрабатывает набор классов, определяет для каждого метода входные и выходные условия и, проверяя их выполнение, доказывает правильность программы. Второй программист затем создает подклассы исходных классов, переопределяя некоторые методы. Если наша точка зрения на подклассы соответствует условиям верификации, то для экземпляра подкласса условия также будут выполнены. Тогда мы заменим в программе экземпляры класса экземплярами подкласса и можем надеяться, что получившаяся программа останется правильной. Мы пришли к принципу подстановки, обсуждавшемуся в главе 10.

Комментарий
15 Верифицирование — специальная техника разработки программ, подробно описанная в [Gries 1981]. При таком подходе в программу добавляются специальные условия, оформленные в виде псевдокомментариев. Выполнение условий считается необходимым для правильной работы программы. Они используются (явно или неявно) программистом при разработке и отладке программы и применяются для математически строгого доказательства правильности программы. Верификации (и в особенности их частный случай — инвариант цикла) вместе с техникой формального доказательства правильности программ предложены Т. Хоаром. Имеются специальные компиляторы, которые в режиме отладки превращают псевдокомментарии во фрагменты кода, проверяющие во время выполнения правильность заданных условий. — Примеч. перев.

В то время как обычная программистская практика и здравый смысл диктуют, что переопределенный метод не должен слишком радикально отклоняться от родительского, в общем случае нет гарантии, что поведение переопределенного метода будет как-либо связано с поведением исходного метода в надклассе. (Как правило, дочерний класс и не будет вести себя точно так же, как родительский — иначе не было бы нужды в переопределении.) Таким образом, у нас нет, вообще говоря, уверенности, что некоторые единые входные и выходные условия будут справедливы для обоих методов. Если одно из этих условий нарушено, основа нашей веры в правильность программы оказывается подорванной, и программа, вероятно, даст сбой. Типичным источником проблемы неоднозначности (см. главу 7) является замена программистом (возможно, непреднамеренно) одного метода другим, для которого не сохранены некоторые важные аспекты поведения.

Пример: пасьянс

Если мы хотим доказать правильность программы карточного пасьянса, представленной в главе 8, то мы должны объяснить работу подпрограммы draw в классе TablePile, которая отображает стопку карт на карточном столе. Чтобы создать изображение, подпрограмма просто проходит в цикле по стопке карт снизу вверх, очищая поверхность под каждой картой и перерисовывая изображение. Таким образом, нижележащие карты прорисовываются, а затем частично стираются по мере того, как рисуется стопка карт.

Предположим, что данное приложение создано и мы обосновали — формально или неформально — его корректность. Теперь новый программист решает, что следует улучшить эффективность подпрограмм рисования, используя параллелизм. Поскольку графические операции являются относительно длительными, то, когда объекту-карте требуется отобразить себя, он попросту запускает фоновый процесс, осуществляющий операцию рисования, и продолжает работу в параллельном режиме. Методики, подобные описанным нами в главе 7, делают такую замену легкой для программирования. Все, что требуется — это создать новый класс (например, ParallelCard), который наследует все из класса Card и переопределяет только метод draw. Описание такого класса показано ниже. Затем программист может сменить ссылки на класс Card в инициализирующей части программы, чтобы использовать вместо него класс ParallelCard.

class ParallelCard : public Card
{
  // наследует все полностью, но изменяет рисование
public:

    void draw()
     {
      if (!fork())  // порождаем процесс
       { Card::draw(); exit(0); }
     }
};

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

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

Данный пример иллюстрирует проблемы, возникающие с появлением наследования. Исходная программа была разработана и тщательно протестирована, и, возможно, некий программист поддастся соблазну думать, что до тех пор, пока выдерживается критерий «быть экземпляром», ранее проведенное тестирование будет оставаться справедливым. К сожалению, данное предположение в общем случае несправедливо, и любое изменение должно подвергаться регрессионному тестированию [Perry 1990].

Использование контролируемых условий

Объектно-ориентированный язык программирования Eiffel [Meyer 1988a, Rist 1995] по крайней мере частично решает эту проблему. К методам присоединяются так называемые условия. Они наследуются и не могут переопределяться в подклассах (хотя могут дополняться). Компилятор генерирует код, проверяющий выполнение этих условий во время выполнения программы. Тем самым минимальный уровень функциональности метода гарантируется независимо от любых возможных переопределений. Конечно, иногда затруднительно сформулировать некоторые условия — такие, как утверждение о том, что игральная карта полностью нарисована перед выходом из метода.

Заметим, что Java предлагает интересный вариант решения этой проблемы: использование модификатора final. Класс, который объявлен как final, не может порождать подклассы, и тем самым гарантируется сохранение его функциональности.

20.1.2. Наследование и память

Классы как типы

Наконец рассмотрим связь между типами данных и управлением памятью. Здесь тоже наследование приводит к тонким нюансам, которые отсутствуют для традиционных типов данных (таких, как записи и массивы). Поскольку с точки зрения «классы суть записи» экземпляр подкласса некоторого класса является расширением экземпляра исходного класса, то он может, конечно же, занимать больше места в памяти. Однако, как мы видели в главе 12, это изменение в размере усложняет кажущийся тривиальным предмет обсуждения — присваивание. Вспомним, что при присваивании переменной родительского класса значения типа подкласса требуется, чтобы:

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

20.2. Классы как объекты

Разделы

Мы подчеркивали выше, что основная философия объектно-ориентированного программирования — делегирование полномочий индивидуальному объекту. Он ответственен за свое внутреннее состояние и изменяет его в соответствии с несколькими фиксированными правилами поведения. С другой стороны, каждое действие должно быть обязанностью некоторого объекта, или же оно не будет выполнено. Несомненно, создание новых экземпляров класса есть некое действие. Вопрос в том, кто (то есть какой объект) должен за это отвечать?

20.2.1. Фабрики по созданию объектов

Классы как объекты

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

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

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

20.2.2. Класс Class

Классы как объекты

Каждый объект должен быть экземпляром некоторого класса, и описанный выше объект не является исключением. Экземпляром какого же класса он является? Ответ для языков Smalltalk, Objective-C, Java и аналогичных им состоит в том, что он является экземпляром класса, называемого Class. На рис. 20.2 показана CRC-карточка (обе стороны) класса Class в системе Little SmallTalk. По соглашению, значение этого объекта содержится в переменной, которая имеет то же самое имя, что и сам класс. То есть переменная Set содержит объект со структурой, аналогичной изображенной на рис. 20.3. Он отвечает за создание новых экземпляров класса.


Рис. 20.2. CRC-карточка для класса Class


Рис. 20.3. Связь экземпляров и связь подклассов

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

Чтобы понять все это, вы должны различать взаимосвязь подклассов и взаимосвязь экземпляров. Класс Class является подклассом класса Object — тем самым объект Class указывает на объект Object как на свой надкласс. С другой стороны, объект Object является экземпляром класса Class — тем самым Object указывает обратно на Class. Класс Class является классом сам по себе и тем самым является экземпляром самого себя. Если мы исследуем типичный класс, скажем Set, то объект Set будет являться экземпляром класса Class, но, кроме того, он будет (неявно) и подклассом класса Object. Конкретное множество set является экземпляром класса Set. Эти взаимоотношения проиллюстрированы на рис. 20.3, на котором сплошные линии представляют взаимосвязь экземпляров, а пунктирные линии обозначают связь подклассов.

20.2.3. Метаклассы и класс-методы

Классы как объекты

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

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

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

Метакласс является классом классов. В Smalltalk-80 метакласс неявно и автоматически конструируется для любого класса, определенного пользователем. Каждый метакласс имеет только один экземпляр, который является собственно классом. Метаклассы организованы в иерархию «класс–подкласс», которая отражает аналогичную иерархию для исходного класса. Эта иерархия содержит метаклассы и имеет корень в классе Class, а не в классе Object.

Ниже показана часть иерархии классов в Smalltalk-80:

Object     — надкласс всех объектов
Collection — абстрактный надкласс всех совокупностей 
Bag        — класс мультимножеств

Соответствующая иерархия метаклассов выглядит так:

Object               — надкласс всех объектов
Class                — поведение, общее для всех классов
Metaclass-Object     — инициализация всех объектов
Metaclass-Collection — инициализация коллекций collections
Metaclass-Bag        — инициализация мультимножеств bags

Код, специфический для отдельного класса, связывается с метаклассом этого класса. Например, экземпляры класса Bag содержат словарь, который используется для хранения фактических значений. Метакласс для класса Bag переопределяет

Листинг 20.1.
Метод newDay:year: класса Date
newDay: dayСount year: referenceYear
  " Возвращает дату, отстоящую на dayСount дней "
  " от начала года referenceYear "
Ѕ day year daysInYear Ѕ
day <- dayCount.
year <- referenceYear.

[ day > (daysInYear <- self daysInYear: year) ]
  whileTrue:
    [ year <- year + 1
     day <- day — daysYear ].

[day <= 0]
  whileTrue:
    [ year <- year — 1
     day <- day + (self daysInYear: year) ]

^ self new day: day year: year

Листинг 20.2.
Метод daysInYear: класса Date
daysInYear: yearInteger
  " возвращает число дней в году, yearInteger "
  ^ 365 + (self leapYear: yearInteger)

leapYear: yearInteger
  " 1, если год yearInteger — високосный, "
  " иначе 0 "

( yearInteger \\ 4 ~= 0 or:
  [ yearInteger \\ 100 = 0 and:
  [ yearInteger \\ 400 ~= 0 ]])
      ifTrue:  [ ^ 0 ]
      ifFalse: [ ^ 1 ]

метод new, чтобы производить инициализацию словаря при создании новых экземпляров. Это действие осуществляется методом new, определенном в классе Metaclass-Bag и переопределяющем метод класса Class. Сам метод приведен ниже:

new
  " создать и инициализировать новый экземпляр "
  ^ super new setdictionary

Поскольку класс Bag является экземпляром класса Metaclass-Bag, то указанный метод будет вызываться в ответ на сообщение new. Прежде всего, метод пересылает сообщение надклассу, который осуществляет действия, аналогичные обязанностям, показанным на CRC-карте, приведенной ранее. Таким способом надкласс создает новый объект. Как только новый объект возвращен, ему посылается сообщение setDictionary. Соответствующий метод, показанный ниже, устанавливает значения полей экземпляра для вновь созданного словаря:

setDictionary
  " установить новый словарь "
  contents <- Dictionary new

Не вы одни считаете эти рассуждения запутанными. Концепция метаклассов имеет репутацию одного из наиболее труднодоступных аспектов в Smalltalk-80. Несмотря на это, она полезна тем, что позволяет нам придать конкретные свойства функции инициализации для индивидуальных классов, не выходя за рамки чистого объектно-ориентированного подхода. Однако, учитывая запутанную природу метаклассов, большинство программистов очень благодарны системе, которая позволяет их не замечать. Например, в Smalltalk-80 класс-методы определяются при просмотре броузером базового класса, а не метакласса. Щелчок «класса» или «экземпляра» во втором окошке показывает, описывается ли метакласс или собственно класс. Аналогично в языке Objective-C методы, перед которыми в первой колонке стоит знак «плюс» (так называемые методы-«фабрики»), связаны с метаклассами, в то время как методы, которые начинаются с «минуса», являются класс-методами.

Дальнейшая информация о метаклассах и метапрограммировании может быть найдена в работе [Kiczales 1991].

20.2.4. Инициализация объектов

Классы как объекты

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

В качестве примера используем класс Date для языка Smalltalk-80. Экземпляры класса представляют собой дату определенного года. Каждый экземпляр класса Date хранит два значения: номер года и число в диапазоне от 1 до 366, то есть день. Новый экземпляр класса Date может быть создан различными способами. Например, сообщение Date today порождает экземпляр класса Date, представляющий текущую дату. Даты могут быть также в явном виде определены пользователем путем задания года и дня. В этом случае вызывается код, показанный в листинге 20.1. Он осуществляет небольшую проверку, гарантируя, что номер дня положителен и не превосходит числа дней в году. Когда есть уверенность, что значения допустимы, код использует метод new для создания нового объекта и инициализации его заданными значениями.

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

20.2.5. Подстановки в Objective-C

Классы как объекты

Интересной иллюстрацией точки зрения «классы суть объекты» является понятие подстановки класса в языке Objective-C. Мы часто видели ситуации, когда программист желает подставить вместо одного класса другой в уже существующем приложении.

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

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

[ GraphicalReactor poseAs: [ Reactor class ]];

Все последующие ссылки на класс Reactor, включая сообщения о создании экземпляров класса, будут отсылаться к классу GraphicalReactor. Наиболее часто объект, подставляемый вместо другого, — это подкласс замещаемого класса. Тем самым большинство сообщений будет переадресовано обратно исходному классу (являющемуся теперь надклассом).

Листинг 20.3.
Описание класса со статическими полями
//
// --------- класс Card
//
class Card
{
public:
    // конструктор
    Card(int s, int c);

    // константы
    static const int CardWidth;
    static const int CardHeight;
...
};

const int Card::CardWidth = 68;
const int Card::CardHeight = 75;

20.3. Данные класса

Разделы

Независимо от того, какого взгляда на классы мы придерживаемся, часто желательно иметь область данных, которая является общей для всех экземпляров класса. Например, все окна Windows можно разместить в одном связном списке или все карты Cards — в единой колоде.

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

Тонкость состоит в инициализации переменных класса. В определенном смысле все экземпляры класса равны: они одинаково функционируют, и имеется только одна копия любой переменной класса. Однако необходимо:

Как в языке Smalltalk, так и в Objective-C система гарантирует, что сообщение initialize пересылается прежде любого другого. Отклик на метод initialize может быть использован затем для установления значения любой переменной класса. Забота о сообщении initialize обеспечивается неявным образом, и здесь нет необходимости вообще в явном виде вызывать метод initialize. Язык Java имеет аналогичное средство, только в этом случае блок инициализации даже не имеет имени.

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

20.3.1. Переменные класса в Smalltalk

Данные класса

Мы определяем переменные класса, просто перечисляя их по именам при создании нового класса. Например, ниже показано описание класса Date. Как сказано в подразделе 20.2.4, экземпляры класса Date используются для представления даты. Переменные класса содержат несколько массивов информации, которая полезна при манипулировании с датами, включая число дней в месяцах невисокосных лет, названия месяцев и дней недели и т. д. Внутренним образом такие переменные обрабатываются как переменные экземпляра метакласса, связанного с классом. Инициализация переменных класса выполняется как часть метода класса initialize.

Magnitude subclass: #Date
  instanceVariableNames: 'day year'
  classVariableNames:

'DaysInMonth FirstDayOfMonth MonthNames SecondsInDay
    WeekDayNames'

  poolDictionaries: ''
  category: 'Numeric-Magnitudes'

20.3.2. Переменные класса в C++

Данные класса

Язык C++ особым образом воспринимает ключевое слово static, когда оно используется в описании класса. Здесь это слово подразумевает, что создается одна копия значения, которая используется совместно всеми экземплярами класса. Такие значения (переменные класса в нашей терминологии) называются статическими элементами в C++. Обычные правила видимости (определяемые ключевыми словами private, protected или public) применяются к статическим элементам для ограничения доступа к ним извне методов, связанных с классами.

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

Поскольку в классе используется только одна копия статического элемента, доступ к открытому (public) статическому элементу может осуществляться напрямую. Например, класс CardPile отображает стопку карт следующим образом:

void CardPile::display()
{
  if (top == nilLink)
    game->clearArea(x, y, x+Card::CardWidth,
                       y+Card::CardHeight);
  else   
    top->draw();
}

Определив поля CardWidth и CardHeight класса Card таким образом, мы избегаем создания отдельных констант в каждом экземпляре класса.

Заметьте, что статические элементы не обязаны быть объявлены открытыми — если они не открыты, то их доступность подчиняется обычным правилам видимости. Язык C++ также допускает, чтобы методы были объявлены как static. Статические методы могут обращаться только к статическим данным и во многих отношениях похожи на класс-методы в языках Smalltalk и Objective-C.

20.3.3. Переменные класса в Java

Данные класса

Язык Java, следуя C++, использует ключевое слово static для указания переменной класса. Поля данных и методы, описанные в стеке, могут быть помечены как static, тогда они будут применимы к самому классу, а не к его экземплярам.

Статические переменные могут иметь инициализаторы, которые выполняются при загрузке определения класса. Кроме того, блок кода разрешается помечать как static, тогда он также будет выполняться во время загрузки. Следующий пример иллюстрирует это. Здесь описывается статический массив значений, который инициализируется числами от 1 до 12:

class statTest
{
  	static final size = 12;
  	static int arr[] = new int[size]; // объявить массив
  	static
     {
      // эти команды выполняются при загрузке класса
      for (int i = 0; i < arr.length; i++)
       {
        arr[i] = i + 1;
       }
     }
}

Комбинация ключевых слов static и final создает инициализированное поле, которому не может быть присвоено значение.

Как статические переменные, так и статические методы обычно доступны через имя класса. Однако для удобства к ним можно обратиться и через экземпляр класса.

20.3.4. Переменные класса в Objective-C

Данные класса

В языке Objective-C нет явной поддержки переменных класса. Мы можем получить нечто похожее, объявляя простые статические (static) переменные языка C в части implementation описания класса. Такие переменные доступны только внутри файла, содержащего реализацию, и к ним нет доступа у клиентов-подклассов и у клиентов-пользователей. Например, методы в нижеследующем классе Date могут ссылаться на статический массив dayNames. (Этот трюк работает также в C++ до тех пор, пока все функции, обращающиеся к данным, находятся в одном файле.)

# import "Date.h"
static char *dayNames[ ] = {
              "Воскресенье", "Понедельник", 
              "Вторник", "Среда", "Четверг",      
              "Пятница", "Суббота"
                           };
@implementation Date
...
@end

20.4. Нужны ли классы?

Разделы

Учитывая все нюансы объектов, экземпляров, классов, метаклассов и тому подобного, невольно задумаешься, нельзя ли создать объектно-ориентированный язык без привлечения классов? Оказывается, можно, хотя не вполне ясно, чем программирование в таких «деклассированных» объектно-ориентированных языках оказывается проще или быстрее, чем в языках с классами. Также не ясно, являются ли получающиеся программы в чем-то более эффективными.

20.4.1. Что такое знание?

Нужны ли классы?

Объектно-ориентированный подход связан с тем представлением, что знание есть отнесение конкретной вещи к абстрактному типу. Платон, например, утверждал, что наше понятие лошади не основывается на какой-либо конкретной лошади, но выводится из идеи «лошадности». Все физические объекты — только приближения к более совершенной абстракции; они — всего лишь бледная тень идеи.

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

20.4.2. Делегирование полномочий

Нужны ли классы?

В языках программирования идея общности между конкретными экземплярами известна как делегирование [Lieberman 1986]. При делегировании нет классов — вместо этого программист создает конкретные экземпляры объектов, и вся функциональность связывается с конкретными объектами. Всякий раз, когда объект оказывается похожим на уже существующий объект, программа может делегировать часть поведения нового объекта объекту-оригиналу. Любое сообщение, которое не распознается новым объектом, будет переадресовано объекту, которому осуществлено делегирование. Он в свою очередь может делегировать свое поведение другому объекту и т. д.

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

В качестве иллюстрации построим простую графическую систему. Предположим, что у нас есть объект, который рисует линии и реагирует на единственное сообщение drawFromTo(a, b, c, d). В качестве реакции на это сообщение рисуется отрезок сплошной линии из точки с координатами (a,b) к точке с координатами (c,d).

Прежде всего мы построим перо, являющееся инструментом рисования, которое помнит свои координаты. Объект-перо инкапсулирует две переменные, x и y, а также определяет методы установки и сообщения этих переменных: getX(), getY(), setX(a), setY(b). Затем объект-перо определяет два метода рисования — а именно, moveTo(a,b), который только перемещает перо без рисования, и drawTo(a,b), который рисует линию. Эти методы могут быть определены с помощью следующего псевдокода:

method moveTo(a, b)
    self setX(a)
    self setY(b)
end

method drawTo(a, b)
    self drawFromTo(self getX(), self getY(), a, b)
    self moveTo(a,b)
end

Объект-перо делегирует ответственность за метод drawFromTo объекту-линии .

Предположим, что программист хочет создать второе перо. Используя технику делегирования, он прежде всего обеспечивает описание объекта, соотнося его (по возможности) с уже существующими объектами. Одна из форм описания может выглядеть так: "второе перо должно вести себя в точности как первое, но поддерживать свои собственные координаты". Из этого описания ясно, что второе перо обязано содержать свои собственные переменные и определять методы для setX и т. д. Однако поскольку оно делегирует себя первому перу, это будут единственные методы, которые надо определить; оставшиеся детали поведения будут унаследованы от первого пера. Когда второму перу посылается сообщение, то получатель (второе перо) пересылается как составная часть сообщения дальше по пути делегирования. Когда следующие сообщения посылаются объекту self (клиенту в терминологии Либермана), поиск начинается снова с исходного получателя. Тем самым сообщения setX и getX, — используемые, например, в методе drawTo, будут сопоставляться с методами второго пера, а не первого. Этот процесс сопоставления аналогичен способу, при котором связывание метода всегда начинается с базового класса получателя. Проблемы, возникающие при этом способе, проявляются и в своем "делегированном" эквиваленте.

Делегирующие объекты не всегда должны переопределять переменные. Предположим, мы хотим создать калейдоскопическое перо, которое производит отражение относительно осей x и y, рисуя четыре линии вместо одной линии для исходного пера . Мы можем ввести объект, который переопределяет только метод drawTo; все остальное поведение делегируется первоначальному перу. Поскольку координаты x и y являются координатами исходного пера, изменения в пере типа "калейдоскоп" приводят к изменениям в первоначальном пере. Новый метод drawTo выглядит следующим образом:

method drawTo(a, b)
  self drawFromTo(self getX(), self getY(), a, b)
  self drawFromTo(- self getX(), self getY(), — a, b)
  self drawFromTo(self getX(), — self getY(), a, — b)
  self drawFromTo(- self getX(), — self getY(), — a, — b)
  self moveTo(a,b)
end

Теперь предположим, что программист хочет определить перо типа «черепашка», которое сохраняет не только координаты, но и ориентацию [Abelson 1981]. Кроме собственно рисования, "черепашку" можно научить поворачиваться и двигаться вперед или назад относительно текущей ориентации. Если мы используем существующее перо для сохранения координат "черепашки", ей потребуется определить единственную переменную, а также методы turn(amount), forward(amount) и backward(amount).

Интересным свойством систем, основанных на принципе делегирования, является их способность динамически изменять делегатов. После того как было сконструировано устройство "черепашка", а затем пользователь перенес делегирование с обычного пера на перо dasedPen, "черепашка" внезапно развивает в себе способность рисовать не только сплошные, но и пунктирные линии. (Естественно, делегирование к объекту, который не понимает всех требуемых сообщений, может привести к краху программы.)

В определенном смысле отношения объект/делегат подобны отношениям экземпляр/класс, за исключением того, что в первом случае нет двух сущностей — делегат просто является другим объектом. Тем не менее существует распространенная практика создания классо-подобных объектов-фабрик, которые не делают ничего, кроме дублирования существующего объекта. Например, удается произвести "фабрику черепашек", которая по запросу выпускает новую "черепашку", независимую от всех других "черепашек".

Основной литературой по делегированию является работа [Lieberman 1986]. Его статья показывает, как с помощью делегирования мы можем смоделировать механизм наследования. Обратное утверждение "с помощью наследования удается смоделировать делегирование" также было продемонстрировано [Stein 1987]. Язык программирования Self, основанный на делегировании, был описан Унгаром [Ungar 1987]. Томлинсон [Tomlinson 1990] приводит интересный анализ затрат времени и памяти при делегировании и при наследовании, приходя к заключению, что наследование в общем случае является более быстрым и, как ни удивительно, требует меньше памяти.

Упражнения

Разделы

  1. Какие аспекты понятия тип данных не описываются категориями Вегнера [Wegner 1986]? Определите новые точки зрения, чтобы отразить эти аспекты.
  2. Изучите технические приемы верификации в стандартных языках программирования (хорошее объяснение их приводится в работах [Gries 1981, Dijkstra 1976]). С какими проблемами вы сталкиваетесь, когда пытаетесь применить эти подходы к

Новый взгляд на классы

Сайт создан в системе uCoz