Лекция 10 (Лекции (2009) (Саша Федорова))
Описание файла
Файл "Лекция 10" внутри архива находится в папке "Лекции (2009) (Саша Федорова)". Документ из архива "Лекции (2009) (Саша Федорова)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Лекция 10"
Текст из документа "Лекция 10"
11
Лекция 10.
В прошлый раз мы рассмотрели модульную структуру Modula-2, Delphi, Oberon.
В Delphi и в Turbo Pascal схема та же самая, только UNIT и Implementation.
Однако отсутствует стадия потенциальной видимости: для использования модуля обязательно надо писать
uses M1;
Существует единая конструкция модуля, но структура пространства имен та же самая.
MODULE M1;
TYPE T * =
Единственная форма импорта в Оберон:
IMPORT M1;
(означает подкачку соответствующей таблицы имен)
В результате процесс трансляции крайне прост.
Вообще говоря, за все надо платить. За простоту мы платим эффектами масштабирования (когда проекты большие, и файлов много – счет модулей идет на тысячи. Ориентироваться в такой структуре, понятное дело, очень тяжело). Рассмотрим случай, когда структура модулей в проектах древовидная. Как проектируются системы такого рода?
Существует два подхода к проектированию древовидной иерархии модулей:
-
сверху вниз(top-down- подход) - сначала проектируется модуль верхнего уровня, а затем мы опускаемся до более низких уровней.
-
снизу вверх(bottom-up- подход) – сначала проектируются самые нижние модули – они в данной иерархии инкапсулированы и ничего не знают о вышестоящих модулях. Подставляют виртуальные сервисы универсального характера, которые нужны всем.(P. S. Современные объектно-ориентированные системы в данном случае предлагают нам сетевые структуры.) на их базе строятся сервисные модули более высоких уровней – и так далее, пока не дойдем до единого главного модуля всей системы. Недостаток такого подхода: мы никогда точно не знаем, что нам понадобится в будущем.
Проект, который реализуется по второй системе, зачастую содержит много лишнего и задерживается со сроками. Наибольшая проблема в том, что заказчик, как правило, не знает, что хочет, и надо быть готовым к тому, что прикладная система в процессе проектирования будет изменяться.
В случае подхода top-down в процессе проектирования происходит постепенное уточнение. При двух этих подходах структура проекта, очевидно, будет различной: при проектировании снизу-вверх проектные модули будут более универсальными, а при проектировании сверху вверх – более специфичны.
Существует подход, при котором программа представляет собой линейную последовательность модулей, и все модули равноправны. В данном случае может возникнуть проблема кольцевых ссылок:
Итог: при проектировании снизу вверх мы получаем набор из изолированных модулей, которые могут использоваться в любом контексте. Однако результат всегда будет в самом конце. В реальности применяются смешанные подходы.
Модульная структура языка ADA
Аналог библиотечного модуля в Аде называется пакетом. На первый взгляд аналогия кажется точной:
Пакет состоит из двух частей: спецификации пакета (аналог модуля определения в Modula-2) и тела пакета (аналог IMPLEMENTATION в Modula-2).
//спецификация пакета
package M is
определение (типов, переменных, констант+ заголовки процедур)
end M;
//тело пакета
package body is
//реализация всех процедур и функций
end M
В Modula-2 тело пакета, как и модуль реализации, предназначается только для реализации.
Отличие Ады в том, что пакеты могут вкладываться.
Замечание. Первый аналог логического модуля – это процедура или функция. Уже начиная с Алгол-60 процедур и функции можно помещать внутрь других процедур и функций.
Пример (Паскаль):
procedure P;
var X1, X2: T; //доступны для вложенной процедуры PIN
procedure PIN;
var X:T1;
…………………….
end
end
В Аде модули также могут быть вложены друг в друга.
Все стандартные имена в Ада берутся из стандартного пакета package STANDART. Пользовательские пакеты вкладываются внутрь него.
При этом «вложения» устроены таким образом, что если вложены друг в друга спецификации, то вложены и тела.
STANDART
package M1 is
package M12 is
………………………………………………
end M12
package M2 is
package M21 is
………………………………………………
end M21
………………………….
end M2
………… ……………………….
end M1
Пространство имен, таким образом, имеет существенно более сложную структуру: область действия любого имени начинается с его определения. Имена пакетов видимы непосредственно в той области, где они были объявлены.
В пакете М1 видимость обьектов из М12 потенциальная.
Имена экспортируются во вне, но доступны они лишь потенциально. Очевидно, этого недостаточно.
Заметим, что такая структура позволяет программировать именно «сверху вниз». Если структура модуля линейна, то все тела пакетов изолированы, можно в принципе экспортировать имена из области их определений (соответственно стратегии «снизу-вверх»). А из внутренней области видимости мы имеем непосредственный доступ во все объемлющие структуры (соответственно стратегии «сверху вниз»). Однако в данном случае мы платим сложностью:
-
возникает понятия скрытия (если на разных уровнях есть одно и то же имя, то более близкое описание скрывает внешнее). Кроме того, мы допускаем неявный импорт: экспортируем одно имя, а вместе с ним что-то еще(что лучше бы не экспортировать)
-
для иллюстрации следующей проблемы приведем пример:
Пример:
имя enum_T имя константы
Такая проблема возникает в языках, где есть перекрытие операций (во всех известных современных языках, кроме Оберон) - перегрузка, перекрытие имен функций.
С точки зрения модели видимости, в Аде есть препятствие к перегрузке операций: все на свете должно быть определено внутри некоторого пакета. Пусть у нас есть тип vector, для которого мы перекрыли операцию «+». Но она, кроме модуля, в котором была переопределена, никому не нужна. Выходит, что для доступа к данной операции нужно будет уточнять доступ. А уточнение работает только в префиксной форме (*). Т. е., чтобы операция была видна внутри модуля vector, где она описана, надо написать так:
a, b, c: vectors.Vector;
a:=vectors:”+”(b, c);
Очевидно, смысл перегрузки операций пропадает. Такая запись равносильна, к примеру, записи
Vectors.Plus(b, c);
Вопрос перегрузки в математических программах – это критичная технологическая потребность. Поэтому возникает форма импорта, благодаря которой все имена из спецификации пакета становятся непосредственно видимыми в настоящей точке:
use vectors;
a, b, c: vectors;
a, b, c: Vector;
a:=b+c; //и все будет Ok!
(*)Любую перегруженную операцию можно употреблять как в префиксной:a=b+c;
так и в постфиксной форме:
a=operator+ (b, c);
А если аргументы операции +, к примеру, не видимы непосредственно?
(С++ в этом случае разрешает неявный импорт: неявный поиск в других пространствах имен(using namespace)
Рассмотрим такую ситуацию:
Пусть у нас есть пакета: М и М1, в каждых из которых есть имя Х, и, написав в программе
use M;
use M1;
мы обращаемся к имени Х. – возникает конфликт имен!
Общее правило таково: как только возникает конфликт имен, имя становится видимым только через уточняющую форму, и непосредственная видимость исчезает.)
В Ada семантика программы будет меняться в зависимости порядка модулей (и в зависимости от порядка модулей по-разному разрешается конфликт имен!).
Вообще говоря, львиная доля проблем в Аде – это проблемы, связанные с областями видимости. Даже через несколько лет после выпуска стандарта в нем продолжали находить баги. Около 90% из них касалось именно вопроса о конфликтах имен. В ряде компаний в связи с этим даже запретили использовать ключевое слово use.
(неявный импорт:
В С# using
В Java import имя_пакета;//ошибка-нельзя экспортировать пакет в Java!
import имя_класса; //Ok!
import имя_пакета.* ;//Ok! – мы импортируем все имена из пакета
В Pascal uses
Итак, как мы выяснили, use в Ada – крайне ненадежная конструкция: программы продолжают компилироваться, но их семантика при этом сильно меняется. Тут помогает переименование:
a renames b; //a теперь описано уже тут
Можно также переименовать операцию «+»:
function “+”(X, Y: vectors.Vector) return vector renames vectors;
Еще пример переименования:
V renames vectors.Vector
Таким образом, переименование – это еще один вид импорта (к переименованному имени можно обращаться без спецификатора пакета – в том пакете, где оно было переименовано.)
Один из важнейших ресурсов Ада – определение новых типов данных.
DEFINITION MODULE STACKS;
TYPE stack=RECORD
B: Array[0..N] of T;
TOP: [0..N];
END; //конец записи
PROCEDURE PUSH(VAR S: STACK; X: T);
PROCEDURE POP(VAR S:STACK; VAR X: T);
Пусть внутри модуля находятся процедуры
IsEmpty
IsFull
PROCEDURE Init(VAR S: stack);//(инициализация стека)
и переменная
VAR done: Boolean;//сообщающая последний результат операции над стеком
END STACKS;
В модуле определений (модуле реализации) соответствующие процедуры должны быть полностью описаны.
Переменная Done говорит нам о том, выполнилась ли какая-то операция над стеком или нет. В чем недостаток? Дело в том, что мы вынуждены экспортировать переменную на полный доступ – а это не есть хорошо. Выход из данной ситуации:
PROCEDURE Init*(VAR S:stack);
VAR DONE * BOOLEAN
PROCEDURE GetError(): Boolean;
Но тогда переменная DONE может быть испорчена. Функция Get Error, очевидно, должна возвращать значение переменной DONE. Но вызов функции – это нелокальный переход, и с точки зрения современной архитектуры он плох.
(Стандартный прием увеличения быстродействия – конвейеризация. Глубина конвейера в современных процессорах – не более 15 инструкций. Если вычисления выполняются последовательно, конвейер заполняется. С локальными переходами процессор «умеет» бороться, а с нелокальными – нет, они сильно снижают эффективность конвейера).
В Оберон 2 переменную можно экспортировать только на чтение. Вот, как бы все это выглядело на Ада:
package Stacks is
type stack is
record
b: array(0..N) of T;
top: integer :=0;//при объявлении записи top всегда будет обнуляться
procedure Push(s: inout Stack; X: in T);//процедура
Как лучше реализовать Pop – как процедуру или как функцию?
PROCEDURE POP(VAR S:stack): T;//в ADA так писать нельзя!!!!!!
Данная функция обладает побочным эффектом, так как модифицирует значение своего формального параметра.
Изначально в Аде могут быть:
-
функции
-
процедуры
-
процедуры, возвращающие значение – это функции с побочным эффектом! (В конечном итоге от этого отказались, т.к. разрешить функции менять свои формальные параметры очень тяжело)
Таким образом, единственная возможность – определить Pop как процедуру:
Procedure POP(S: inout stack; X: out T);
………….
End Stacks;
Заметим, что возможна ситуация
X: Stack;
X.top=1;//top, напомним, автоматически инициализируется нулем, а мы тут кладем в нее единицу, целостность стека нарушается!
Структура типа очень важна, а она не инкапсулирована!
Поэтому переходим к следующей главе.
Глава 7. Инкапсуляция и абстрактный тип данных
Любой тип данных с современной точки зрения представляет собой совокупность множества значений и множества операций.
Сосредоточимся на механизме реализации инкапсуляции.
РОРИ – принцип разделения интерфейса и реализации.
Абстрактный тип данных - это набор процедур, объявленный в отдельном пакете. Здесь, оказывается, есть инкапсуляция:
-
единицы инкапсуляции: тип или экземпляр типа
-
атомы инкапсуляции: отдельные поля и члены типа или весь тип.
В большинстве языков программирования единица инкапсуляции – тип.
В языке Оберон есть защита отдельных членов, что позволяет по отдельности экспортировать отдельные поля.
Ада и Modula-2 инкапсулируют целиком весь тип: такая тактика вынуждает нас к полной инкапсуляции.