учебники, программирование, основы, введение в,

 

Глобальные объекты и константы

Константы базовых типов
Начнем с формы записи констант.
Правило стиля - принцип символических констант - гласит, что обращение к конкретному значению (числу, символу или строке) почти всегда должно быть косвенным. Должно существовать определение константы, задающее имя, играющее роль символической константы (symbolic constant), и связанное с ним значение - константа, называемаю манифестной (manifest constant). Далее в алгоритме следует использовать символическую константу. Тому есть два объяснения.

  • Читабельность: читающему текст легче понять смысл US_states_count, чем числа 50;
  • Расширяемость: символическую константу легко обновить, исправив лишь ее определение.

Принцип допускает применение манифестных или, как часто говорят, неименованных констант в качестве "начальных" элементов разнообразных операций, как в случае с циклом from i = 1 until i > n (Но n, конечно, должно быть символической константой).
Итак, нам нужен простой и ясный способ определения символических констант.
Атрибуты-константы
Как и все сущности, символические константы должны быть определены внутри класса. Будем рассматривать константы как атрибуты с фиксированным значением, одинаковым для всех экземпляров класса.
Синтаксически вновь используем служебное слово is, применяемое при описании методов, только здесь за ним будет следовать не алгоритм, а значение нужного типа. Вот примеры определения констант базовых типов INTEGER, BOOLEAN, REAL и CHARACTER:
Zero: INTEGER is 0
Ok: BOOLEAN is True
Pi: REAL is 3.1415926524
Backslash: CHARACTER is '\'

Как видно из этих примеров, имена атрибутов-констант рекомендуется начинать с заглавной буквы, за которой следуют только строчные символы.
Потомки не могут переопределять значения атрибутов-констант.
Как и другие атрибуты, класс может экспортировать константы или скрывать. Так, если C - класс, экспортирующий выше объявленные константы, а у клиента класса к сущности x присоединен объект типа C, то выражение x.Backslash обозначает символ '\'.
В отличие от атрибутов-переменных, константы не занимают в памяти места. Их введение не связано с издержками в период выполнения, а потому не страшно, если их в классе достаточно много.
Использование констант
Вот пример, показывающий, как клиент может применять константы, определенные в классе:
class FILE feature
error_code: INTEGER;         -- Атрибут-переменная
Ok: INTEGER is 0
Open_error: INTEGER is 1
...
open (file_name: STRING) is
-- Открыть файл с именем file_name
-- и связать его с текущим файловым объектом
do
error_code := Ok
...
if "Что-то не так" then
error_code := Open_error
end
end
... Прочие компоненты ...
end

Клиент можем вызвать метод open и проверить успешность операции:
f: FILE; ...
f.open
if f.error_code = f.Open_error then
"Принять меры"
else
...
end

Нередко нужны и наборы констант, не связанных с конкретным объектом. Их, как и раньше, можно объединить в класс, выступающий в роли родителя всех классов, которым необходимы константы. В этом случае можно не создавать экземпляр класса:
class EDITOR_CONSTANTS
feature
Insert: CHARACTER is 'i'
Delete: CHARACTER is 'd'; -- и т.д.
...
end
class SOME_CLASS_FOR_THE_EDITOR
inherit
EDITOR_CONSTANTS
...Другие возможные родители ...
feature ...
... подпрограммы класса имеют доступ к константам, описанным в EDITOR_CONSTANTS ...
end

Класс, подобный EDITOR_CONSTANTS, служит лишь для размещения в нем группы констант, и его роль как "реализации АТД" (а это - наше рабочее определение класса) не столь очевидна, как в предыдущих примерах. Теоретическое обоснование введения таких классов мы обсудим позднее. Представленная схема работоспособна только при множественном наследовании, поскольку классу SOME_CLASS_FOR_THE_EDITOR могут потребоваться и другие родители.

Константы пользовательских классов

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

Константы с манифестом для этого непригодны

Первым примером служит класс, описывающий комплексное число:

class COMPLEX creation
         make_cartesian, make_polar
feature
         x, y: REAL
                  -- Действительная и мнимая часть
         make_cartesian (a, b: REAL) is
                  -- Установить действительную часть a, мнимую - b.
         do
                  x := a; y := b
         end
         ... Прочие методы (помимо x и y, других атрибутов нет) ...
end
            

Пусть мы хотим определить константу - комплексное число i, действительная часть которого равна 0, а мнимая 1. Первое, что приходит в голову, - это буквальная константа вида

i: COMPLEX is "Выражение, определяющее комплексное число (0, 1)"
            

Как записать выражение после is? Для пользовательских типов данных никакой формы записи неименованных констант не существует.
Можно представить себе вариант нотации на основе атрибутов класса:

i: COMPLEX is COMPLEX (0, 1)
            

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

Однократные функции

Пусть константный объект - это функция. Например, i можно (в иллюстративных целях) описать внутри самого класса COMPLEX как

i: COMPLEX is
         -- Комплексное число, re= 0, а im= 1
do
         create Result.make_cartesian (0, 1)
end
            

Это почти решает нашу задачу, поскольку функция всегда возвратит ссылку на объект нужного вида. Коль скоро мы полагаемся на обычную процедуру создания объекта, условие инварианта будет соблюдено, - как следствие, получим корректный объект.
Однако результат не соответствует потребностям: каждое обращение клиента к i порождает новый объект, идентичный всем остальным, а это - трата времени и пространства. Поэтому необходим особый вид функции, выполняемой только при первом вызове. Назовем такую функцию однократной (once function). В целом она синтаксически аналогична обычной функции и отличается лишь служебным словом once, начинающего вместо do ее тело:

i: COMPLEX is
         -- Комплексное число, re= 0, im= 1
once
         create Result.make_cartesian (0, 1)
end
            

При первом вызове однократной функции она создает объект, который представляет желаемое комплексное число, и возвращает на него ссылку. Каждый последующий вызов приведет к немедленному завершению функции и возврату результата, вычисленного в первый раз. Что касается эффективности, то обращение к i во второй, третий и т.д. раз должно отнимать времени ненамного больше, чем операция доступа к атрибуту.
Результат, найденный при первом вызове однократной функции, может использоваться во всех экземплярах класса, включая экземпляры потомков, где эта функция не переопределена. Переопределение однократных функций как обычных (и обычных как однократных) допускается без всяких ограничений. Так, если COMPLEX1, порожденный от класса COMPLEX, заново определяет i, то обращение к i в экземпляре COMPLEX1 означает вызов переопределенного варианта, а обращение к i в экземпляре самого COMPLEX или его потомка, отличного от COMPLEX1, означает вызов однократной функции, то есть значения, найденного ею при первом вызове.

Применение однократных подпрограмм

Понятие однократных подпрограмм расширяет круг задач, позволяя включить разделяемые объекты, глобальные системные параметры, инициализацию общих свойств.
Разделяемые объекты
Для ссылочных типов, таких как COMPLEX, наш механизм фактически предлагает константные ссылки, а не обязательно константные объекты. Он гарантирует, что тело функции выполняется при первом обращении, возвращая результат, который будет также возвращаться при последующих вызовах, уже не требуя никаких действий.
Если функция возвращает значение ссылочного типа, то в ее теле, как правило, есть инструкция создания объекта, и любой вызов приведет к получению ссылки на этот объект. Хотя создание объекта не повторяется, ничто не мешает изменить сам объект, воспользовавшись полученной ссылкой. В итоге мы имеем разделяемый объект, не являющийся константным.
Пример такого объекта - окно вывода информации об ошибках. Пусть все компоненты интерактивной системы могут направлять в это окно свои сообщения:
Message_window.put_text ("Соответствующее сообщение об ошибке")

где Message_window имеет тип WINDOW, чей класс описан следующим образом:
class WINDOW
creation
make
feature
make (...) is
-- Создать окно; аргументы задают размер и положение.
do ... end
text: STRING
-- Отображаемый в окне текст
put_text (s: STRING) is
-- Сделать s отобржаемым в окне текстом.
do
text := s
end
... Прочие компоненты ...
end -- класс WINDOW

Ясно, что объект Message_window должен быть одним для всех компонентов системы. Это достигается описанием соответствующего компонента как однократной функции:
Message_window: WINDOW is
-- Окно для вывода сообщений об ошибках
once
create Result.make ("... Аргументы размера и положения ...")
end

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


Создав разделяемый объект, играющий роль константы, (например, i), вы можете запретить вызовы i.some_procedure, способные его изменять. Для этого, например, в классе COMPLEX достаточно ввести в инвариант класса предложения i.x = 0 и i.y = 1.

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

  • компонентам низкоуровневой системы может понадобиться объем доступной им памяти, выделенный средой при инициализации;
  • система эмуляции терминала может начать работу с отправки среде запроса о числе терминальных портов. Затем эти данные будут использоваться в ряде модулей приложения.

Такие глобальные данные аналогичны совместно используемым объектам, хотя обычно они являются значениями базовых типов. Схема их реализации однократными функциями такова:
Const_value: T is
-- Однократно вычисляемый системный параметр
local
envir_param: T ' -- Любой тип (T и не только)
once
"Получить envir_param из операционной среды"
Result := "Значение, рассчитанное на основе envir_param"
end

Такие однократные функции описывают динамически вычисляемые константы.
Предположим, данное объявление находится в классе ENVIR. Класс, которому надо воспользоваться константой Const_value, получит ее значение, указав ENVIR в списке своих родителей. В отличие от классического подхода к расчету константы, здесь не нужна процедура инициализации системы, вычисляющая все глобальные параметры системы, как это делается в классическом подходе. Как отмечалось в начальных лекциях, такая процедура должна была бы иметь доступ к внутренним деталям многих модулей, что нарушало бы ряд критериев и принципов модульности: декомпозиции, скрытия информации и других. Наоборот, классы, подобные ENVIR, могут разрабатываться как согласованные модули, каждый задающий множество логически связанных глобальных значений. Процесс вычисления такого параметра, к примеру, Const_value, инициирует первый из компонентов, который запросит этот параметр при выполнении системы. Хотя Const_value является функцией, использующие его компоненты могут полагать, что имеют дело с константным атрибутом.
Как уже говорилось, ни один модуль не имеет больше прав на разделяемые данные, чем остальные. Это особенно справедливо для только что рассмотренных случаев. Если расчет значения способен инициировать любой модуль, нет смысла и говорить о том, будто один из них выступает в роли владельца. Такое положение дел и отражает модульная структура системы.
Однократные процедуры
Функция close должна вызываться только один раз. Контроль над количеством ее вызовов рекомендуется возложить на глобальную переменную приложения.
Из руководства к коммерческой библиотеке функций языка C
Механизм однократных функций интересен и при работе с процедурами. Однократные процедуры могут применяться для инициализации общесистемного свойства, когда заранее неизвестно, какому компоненту это свойство понадобится первому.
Примером может стать графическая библиотека, в которой любая функция, вызываемая первой, должна предварительно провести настройку, учитывающую параметры дисплея. Автор библиотеки мог, конечно, потребовать, чтобы каждый клиент начинал работу с библиотекой с вызова функции настройки. Этот нюанс, в сущности, не решает проблему - чтобы справиться с ошибками, любая функция должна обнаруживать, не запущена ли она без настройки. Но если функции такие "умные", то зачем что-то требовать от клиента, когда можно нужную функцию настройки вызывать самостоятельно.
Однократные процедуры решают эту проблему лучше:
check_setup is
-- Настроить терминал, если это еще не сделано.
once
terminal_setup -- Фактические действия по настройке.
end

Теперь каждая экранная функция должна начинаться с обращения к check_setup, первый вызов которой приведет к настройке параметров, а остальные не сделают ничего. Заметьте, что check_setup не должна экспортироваться клиентам.
Однократная процедура - это важный прием, упрощающий применение библиотек и других программных пакетов.
Параметры
Однократные процедуры и функции могут иметь параметры, необходимые, по определению, лишь при первом вызове.
Однократные функции, закрепление и универсальность
В этом разделе мы обсудим конкретную техническую проблему, поэтому при первом чтении книги его можно пропустить.
Однократные функции, тип которых не является встроенным, вносят потенциальную несовместимость с механизмом закрепления типов и универсальностью.
Начнем с универсальности. Пусть в родовом классе EXAMPLE [G] есть однократная функция, чей тип родовой параметр:
f: G is once ... end

Рассмотрим пример ее использования:
character_example: EXAMPLE [CHARACTER]
...
print (character_example.f)

Пока все в порядке. Но если попытаться получить константу с другим родовым параметром:
integer_example: EXAMPLE [INTEGER]
...
print (integer_example.f + 1)

В последней инструкции мы складываем два числа. Первое значение, результат вызова f, к сожалению, уже найдено, поскольку f - однократная функция, причем символьного, а не числового типа. Сложение окажется недопустимым.
Проблема заключается в попытке разделения значения разными формами родового порождения, ожидающими значения, тип которого определяется родовым параметром. Аналогичная ситуация возникает и с закреплением типов. Представим себе класс B, добавляющий еще один атрибут к компонентам своего родителя A:
class B inherit A feature
attribute_of_B: INTEGER
end

Пусть A имеет однократную функцию f, возвращающую результат закрепленного типа:
f: like Current is once create Resultl make end

и пусть первый вызов функции f имеет вид:
a2 := a1.f

где a1 и a2 имеют тип. Вычисление f создаст экземпляр A и присоединит его к сущности a2. Все прекрасно. Но предположим, далее следует:
b2 := b1.f

где b1 и b2 имеют тип B. Не будь f однократной функцией, никакой проблемы бы не возникло. Вызов f породил бы экземпляр класса B и вернул его в качестве результата. Но функция является однократной, а ее результат был уже найден при первом вызове. И это - экземпляр A, но не B. Поэтому инструкция вида:
print (b2.attribute_of_B)

попытается обратиться к несуществующему полю объекта A.
Проблема в том, что закрепление вызывает неявное переопределение типов. Если бы f была переопределена явно, с применением в классе B объявления
f: B is once create Resultl make end

при условии, что исходный вариант f в классе A возвращает результат типа A (а не like Current), все было бы замечательно: экземпляры A обращались бы к версии f для A, экземпляры B - к версии f для B. Однако закрепление типов было введено как раз для того, чтобы избавить нас от таких явных переопределений.
Эти примеры - свидетельства несовместимости семантики однократных функций (с процедурами все прекрасно) с результатами применения закрепленных типов и формальных родовых параметров. Одно из решений проблемы в том, чтобы трактовать такие случаи как явные переопределения, приняв за правило то, что результат однократной функции совместно используется лишь в пределах одной формы родовой порождения, а при закреплении результата - лишь среди экземпляров своего класса. Недостатком такого подхода, впрочем, является, что он не отвечает интуитивной семантике однократных функций, которые, с позиции клиента, должны быть эквивалентны разделяемым атрибутам. Во избежание недоразумений и возможных ошибок можно пойти на более суровые меры, наложив полный запрет на сценарии подобного рода:
Правило для однократной функции
Тип результата однократной функции не может быть закреплен и не может включать любой родовой параметр.

Константы строковых типов

В начале этой лекции были введены символьные константы, значением которых является символ. Например:
Backslash: CHARACTER is '\'

Однако нередко классам требуются строковые константы, использующие, как обычно, для записи константы двойные кавычки:
[S1]
Message: STRING is "Syntax error" -- "Синтаксическая ошибка"

Вспомните, что STRING - не простой тип. Это - библиотечный класс, поэтому значение, связанное с сущностью Message во время работы программы, является объектом, то есть экземпляром STRING. Как вы могли догадаться, такое описание является сокращенной формой объявления однократной функции вида:
[S2]
Message: STRING is
-- Строка из 12 символов
once
create Result.make (12)
Result.put ('S', 1)
Result.put ('y', 2)
...
Result.put ('r', 12)
end

Строковые значения являются не константами, а ссылками на разделяемые объекты. Любой класс, имеющий доступ к Message, может изменить значение одного или нескольких символов строки. Строковые константы можно использовать и как выражения при передаче параметров или присваивании:
Message_window.display ("НАЖМИТЕ ЛЕВУЮ КНОПКУ ДЛЯ ВЫХОДА")
greeting := "Привет!"

Unique-значения
Иногда при разработке программ возникает потребность в сущности, принимающей лишь несколько значений, характеризующих возможные ситуации. Так, операция чтения может вернуть код результата, значениями которого будут признаки успешной операции, ошибки при открытии и ошибки при считывании. Простым решением проблемы было бы применение целочисленного атрибута:
code: INTEGER

и набора символьных констант
[U1]
Successful: INTEGER is 1
Open_error: INTEGER is 2
Read_error: INTEGER is 3

которые позволяют записывать условные инструкции вида
[U2]
if code = Successful then ...

или инструкции выбора
[U3]
inspect
code
when Successful then
...
when ...
end

Но такой перебор значений констант утомляет. Следующий вариант записи действует так же, как [U1]:
[U4]
Successful, Open_error, Read_error: INTEGER is unique

Спецификатор unique, записанный вместо буквального значения в объявлении атрибута-константы целого типа, указывает на то, что это значение выбирает компилятор, а не сам разработчик. При этом условная инструкция [U2] и оператор выбора [U3] по-прежнему остаются в силе.
Каждое unique-значение в теле класса положительно и отличается от других. Если, как в случае [U4], константы будут описаны вместе, то их значения образуют последовательность. Чтобы ограничить значение code этими тремя константами, в инвариант класса можно включить условие
code >= Successful; code <= Read_error

Располагая подобным инвариантом, производные классы, обладающие правом специализации инварианта, но не его расширением, могут сузить, но не расширить перечень возможных значений code, сведя его, скажем, всего к двум константам.
Значения, заданные как unique, следует использовать только для представления фиксированного набора возможных значений. Если допустить его пополнение, то это приведет к необходимости внесения изменений в тексты инструкций, подобных [U3]. В общем случае для классификации не рекомендуется использовать unique-значения, так как ОО-методология располагает лучшими приемами решения этой задачи. Данный выше пример является образцом правильного обращения с описанным механизмом. Правильными можно считать и объявления цветов семафора: green, yellow, red: INTEGER is unique; нот: do, re, mi, ...: INTEGER is unique. Объявление savings, checking, money_market: INTEGER is unique возможно будет неверным, поскольку различные финансовые инструменты, список которых здесь приведен, имеют различные свойства или допускают различную реализацию. Более удачным решением в этом случае, пожалуй, станут механизмы наследования и переопределения.
Объединим сказанное в форме правила:
Принцип дискриминации
Используйте unique для описания фиксированного набора возможных альтернатив. Используйте наследование для классификации абстракций с изменяющимися свойствами.
Хотя объявление unique-значений напоминает определение перечислимых типов (enumerated type) языков Pascal и Ada, оно не вводит новые типы, а только целочисленные значения. Дальнейшее обсуждение позволит объяснить разницу подходов.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика