• Константы базовых типов
  • Атрибуты-константы
  • Использование констант
  • Константы пользовательских классов
  • Константы с манифестом для этого непригодны
  • Однократные функции
  • Применение однократных подпрограмм
  • Разделяемые объекты
  • Однократные функции с результатами базовых типов
  • Однократные процедуры
  • Параметры
  • Однократные функции, закрепление и универсальность
  • Константы строковых типов
  • Unique-значения
  • Обсуждение
  • Инициализация: подходы языков программирования
  • Строковые константы
  • Unique-значения и перечислимые типы
  • Ключевые концепции
  • Библиографические замечания
  • Упражнения
  • У18.1 Эмуляция перечислимых типов однократными функциями
  • У18.2 Однократные функции для эмуляции unique-значений
  • У18.3 Однократные функции в родовых классах
  • У18.4 Однократные атрибуты?
  • Лекция 18. Глобальные объекты и константы

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

    Константы базовых типов

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

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

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

    Начнем с формы записи констант.

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

    [x]. Читабельность: читающему текст легче понять смысл US_states_count, чем числа 50;

    [x]. Расширяемость: символическую константу легко обновить, исправив лишь ее определение.

    Принцип допускает применение манифестных или, как часто говорят, неименованных констант в качестве "начальных" элементов разнообразных операций, как в случае с циклом 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.

    Однократные функции с результатами базовых типов

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

    [x]. компонентам низкоуровневой системы может понадобиться объем доступной им памяти, выделенный средой при инициализации;

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

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


    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 Result make end



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


    a2 := a1.f



    где a1 и a2 имеют тип A. Вычисление 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, оно не вводит новые типы, а только целочисленные значения. Дальнейшее обсуждение позволит объяснить разницу подходов.

    Обсуждение

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

    Инициализация: подходы языков программирования

    Проблема, решаемая в этой лекции, - это общая проблема языков программирования: как работать с глобальными константами и разделяемыми объектами, в частности, как выполнять их инициализацию в библиотеках компонентов?

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

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


    if not ready then

    "Создать или вычислить p"

    ready := True

    end



    Теперь проблема инициализации касается только ready - еще одного глобального объекта, который необходимо инициализировать значением False.

    Как же решается эта задача в языках программирования? С момента их появления в этом плане почти ничего не менялось. В блочно-структурированных языках, среди которых Algol и Pascal, типичным было описание ready как глобальной переменной на верхнем синтаксическом уровне; ее инициализация производилась в главной программе. Но такая техника непригодна для библиотек автономных модулей.

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


    COMMON /common_block_name/ data_item_names



    При этом возникают две проблемы:

    [x]. Две совокупности подпрограмм могут использовать одноименные общие блоки, что приведет к конфликту, если одной из программ понадобится как первый, так и второй блок. Смена имени блока вызовет трудности у других программ.

    [x]. Как инициализировать сущности общего блока, такие как ready? Из-за отсутствия инициализации по умолчанию, ее нужно выполнять в особом модуле, называемом блоком данных (block data unit). В Fortran 77 допускаются именованные модули, что позволяет разработчикам объединять глобальные данные разных общих блоков. При этом есть немалый риск несогласованности инициализации и объявления глобальных объектов.

    Принцип решения этой задачи в языке C по сути не отличается от решения Fortran 77. Признак ready нужно описать как "внешнюю" переменную, общую для нескольких "файлов" (единиц компиляции языка). Объявление переменной с указанием ее значения может содержать только один файл, остальные, используя директиву extern, подобную COMMON в Fortran 77, лишь заявляют о необходимости доступа к переменной. Обычно такие определения объединяют в "заголовочные" (header) .h-файлы, которые соответствуют блоку данных в Fortran. При этом наблюдаются те же проблемы, отчасти решаемые утилитами make, призванными отслеживать возникающие зависимости.

    Решение может быть близко к тому, что предлагают модульные языки наподобие Ada или Modula 2, подпрограммы которых можно объединять в модули более высокого уровня. В Ada эти модули называют "пакетами" (package). Если все подпрограммы, использующие группу взаимосвязанных глобальных объектов, собраны в одном пакете, то соответствующие признаки ready можно описать в этом же пакете и здесь же выполнить их инициализацию. Однако этот подход (применимый также в C и Fortran 77) не решает проблему инициализации автономных библиотек. Еще более деликатный вопрос связан с тем, как поступать с глобальными объектами, разделяемых подпрограммами разных независимых модулей. Языки Ada и Modula не дают простого ответа на этот вопрос.

    Механизм "однократных" методов, сохраняя независимость классов, допускает контекстно-зависимую инициализацию.

    Строковые константы

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

    [x]. STRING рассматривается как встроенный тип, каким он является во многих языках программирования. Это означает введение в язык операций над строками: конкатенации, сравнения, выделения подстроки и других, что усложняет язык. Преимуществом введения такого класса является возможность снабдить его операции точными спецификациями, благодаря утверждениям, и способность порождать от него другие классы.

    [x]. STRING рассматривается как обычный класс, создаваемый разработчиком. Тогда задавать его константы в манифестной форме [S1] уже нельзя, от разработчиков потребуется соблюдение формата [S2]. Кроме того, данный подход препятствует оптимизации компилятором таких операций, как прямой доступ к символам строки.

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

    Unique-значения и перечислимые типы

    Pascal и производные от него языки допускают описание переменной вида


    code: ERROR



    где ERROR - это "перечислимый тип":


    type ERROR = (Normal, Open_error, Read_error)



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

    [x]. Статус символических имен не вполне ясен. Могут ли два перечислимых типа иметь общие символические имена (скажем, Orange в составе типов FRUIT и COLOR)? Можно ли их экспортировать как переменные и распространять на них те же правила видимости?

    [x]. Значения перечислимых типов трудно получать и передавать программам, написанным на других языках, к примеру, C и Fortran, не поддерживающих такое понятие. В тоже время значения, описанные как unique, - это обычные числа, работа с которыми не вызывает никаких проблем.

    [x]. Перечислимые типы данных могут требовать специальных операторов. Так, можно представить себе оператор next, возвращающий следующее значение и неопределенный для последнего элемента перечисления. Помимо него потребуется оператор, сопоставляющий элементу целое значение (индекс). В итоге синтаксическое и семантическое усложнение языка кажется непропорциональным вкладу этого механизма.

    Объявления перечислимых типов в Pascal и Ada обычно принимают вид:


    type FIGURE_SORT = (Circle, Rectangle, Square, ...)



    и используются совместно с вариантными полями записей:


    FIGURE =

    record

    perimeter: INTEGER;

    ... Другие атрибуты, общие для фигур всех типов ...

    case fs: FIGURE_SORT of

    Circle: (radius: REAL; center: POINT);

    Rectangle:... Специальные атрибуты прямоугольника ...;

    ...

    end

    end



    Этот механизм позволяет организовать разбор случаев в операторе выбора case:


    procedure rotate (f: FIGURE)

    begin case f of

    Circle:... Специальные операции поворота окружности ...;

    Rectangle:...;

    ...



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

    Когда это наиболее важное применение перечислимых типов исчезло, все, что осталось необходимым в некоторых случаях, - это выбор целочисленных кодов для фиксированного множества возможных значений. Определив их как обычные целые, мы избежим многих семантических неопределенностей, связанных с перечислимыми типами, например, нет ничего необычного в выражении Circle +1, если известно, что Circle типа integer. Введение unique-значения позволяет обойти единственное неудобство, связанное с необходимостью инициализации значений, позволяя выполнять ее автоматически.

    Ключевые концепции

    [x]. При любом подходе к конструированию ПО возникает проблема работы с глобальными объектами, совместно используемыми компонентами разных модулей, и инициализируемыми в период выполнения, когда какой-либо из компонентов первым к ним обратился.

    [x]. Константы могут быть манифестными и символическими. Первые задаются значениями, синтаксис которых определен так, что значение одновременно описывает и тип константы, а потому является манифестом. Символические константы представлены именами, а их значение указывается в определении константы.

    [x]. Манифестные константы базовых типов можно объявлять как константные атрибуты, не требующие памяти в объектах.

    [x]. За исключением строк, типы, определенные пользователем, не имеют манифестных констант, нарушающих принципы Скрытия информации и расширяемости.

    [x]. Однократная подпрограмма синтаксически отличается от обычной лишь ключевым словом once, заменяющим do. Она полностью выполняется лишь один раз (при первом вызове). При последующих вызовах однократной функции возвращается результат, вычисленный при первом вызове, последующие вызовы процедуры не имеют эффекта и могут быть проигнорированы.

    [x]. Разделяемые объекты могут быть реализованы как однократные функции. Можно использовать инвариант для указания их константности.

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

    [x]. Тип однократной функции не может быть закрепленным или родовым типом.

    [x]. Константы строковых типов внутренне интерпретируются как однократные функции, однако, внешне они выглядят как манифестные константы, значения которых заключается в двойные кавычки.

    [x]. Перечислимые типы в стиле языка Pascal не соответствуют объектной методологии. Для представления объектов с несколькими возможными вариантами значений используются символические unique константы. Инициализация значений таких констант выполняется компилятором.

    Библиографические замечания

    Проблемы перечислимых типов были изучены в работах [Welsh 1977] и [Moffat 1981]. Некоторые приемы, рассмотренные в этой лекции, впервые представлены в [M 1988b].

    Упражнения

    У18.1 Эмуляция перечислимых типов однократными функциями

    Покажите, что при отсутствии unique-типов перечислимый тип языка Pascal


    type ERROR = (Normal, Open_error, Read_error)



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

    У18.2 Однократные функции для эмуляции unique-значений

    Покажите, что в языке без поддержки unique-объявлений результат, аналогичный


    value: INTEGER is unique



    можно получить, воспользовавшись объявлением вида


    value: INTEGER is once...end



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

    У18.3 Однократные функции в родовых классах

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

    У18.4 Однократные атрибуты?

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







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх