лекции (2004) (1160823), страница 14
Текст из файла (страница 14)
В Обероне структура пространства имён остается точно такой же. Общее пространство имен – имена модулей, которые видимы непосредственно. В Обероне это единственные объекты, которые видимы непосредственно. Все остальные имена, которые экспортируются при помощи конструкции "*", они видны только потенциально (M1.имя). снять эту потенциальную видимость нельзя.
Существует единственная конструкция импорта:
IMPORT список_имён_модулей
В момент загрузки модуля подгружается таблица имен для соответствующего модуля, имена, помеченные звездочкой – становятся потенциально видимы. Всякого рода средства снятия видимости являются необязательными. Оберон - единственный из современных языков, в котором отсутствует понятие перекрытия имён. Каждое имя в одном контексте обозначает один и только один объект. В современных языках – это не так, у нас могут быть две одноименные функции, которые должны различаться профилем параметров, - но это относится только к функциям. В Обероне даже функции и процедуры нельзя перекрывать, единственно исключение из этого правила - это понятие динамически связанных методов (позже).
На это закончим с простейшей моделью видимости, которая обобщает модель видимости языка ассемблер, и рассмотрим модель видимости языка Ада.
4) В языке Ада есть понятие пакета. Пакет делится на 2 части: спецификация и тело пакета. Идейно это похоже на модуль определений и модуль реализаций языка Модула-2.
-
Спецификация пакета - только объявления и заголовки
package P is
объявление // здесь могут быть вложенные пакеты, основное отличие от ранее рассмотренных языков
[приватная часть] // необязательна
end P.
-
Тело пакета: для каждой спецификации может существовать тело пакета (а может и не существовать - как в Модуле-2):
package body P is
реализация процедур и функций из спецификации
end P;
Если есть вложенные пакеты, то тела вложенных спецификаций находятся внутри тела пакета.
Чем отличаются вложенные пакеты от глобальных? Рассмотрим два стандартных подхода проектирования программных систем:
1. Нисходящий (top-down) - вначале проектируем модуль верхнего уровня, специфицируем, что нужно от других уровней и их проектируем. Соответствует технологии структурного программирования, каждый модуль представляется как некий черный ящик, который начинают детализировать только на очередном уровне разработке.
2. Восходящий (bottom-up) - сначала проектируем модули самого нижнего уровня (например, те которые занимаются вводом-выводом), а потом на их основе проектируем модули более высокого уровня, и т.д. пока не спроектируем самый главный модуль. Сначала проектируем сервисные модули, потом поднимаемся дальше, пока не спроектируем модуль высшего уровня. При дизайне ОС – такой подход весьма продуктивный, обеспечивает расслоение – выделяет уровни абстракций, позволяет свести интерфейс между этими уровнями абстракций до разумного минимума. Очевидный недостаток - главный модуль появляется самым последним (что не очень хорошо для отчёта перед заказчиком).
В чистом виде эти методы появляются редко, чаще всего используется их совокупность. Так главным является разработка именно такого продукта, который хочет клиент, и важно найти недоработки и начать рефакторинг как можно раньше.
Когда программа вытягивается в линейную последовательность сервисных модулей. Это соответствует восходящей последовательности. У нас есть глобальное пространство имен, и модули добавляют в это пространство свои экспортируемые имена. В такой системе клиентский модуль (использует импорт) знает о существовании сервисных модулей, а сами сервисные модули ничего не знают о клиентских. Это одностороннее связывание модулей.
В большинстве яп существующая технология проектирования модулей она поддерживает в явном виде только восходящий стиль программирования.
При нисходящем стиле сервисный модуль знает о клиентских, возможная ситуация, которая не возможная при восходящем программировании.
package P is
package P1 is
package P11 is
end Р11;
end P1;
end P;
Спрашивается: к каким ресурсам могут обращаться эти пакеты? Что видят Р1 и Р11? Они могут ссылаться не только на ресурсы друг друга, но и на ресурсы модуля, который их содержит. Более того сами тела пакетов могут ссылаться на объекты, которые описаны в теле пакета. Этого нельзя достигнуть при использовании схемы с односторонним связыванием, там имена, описанные в модуле реализации недоступны никому, тут они доступны реализациям внутренних пакетов. Здесь связь двусторонняя – Р является клиентским по отношению к Р1 и Р11, и в то же время Р1 и Р11 являются клиентскими по отношению к Р (могут использовать как внешние так и внутренние ресурсы). Такая технология проектирования поддерживает нисходящий метод.
Большинство современных ЯП не поддерживают вложенную структуру. Классы могут быть вложенными, но учитывая практику использования таких концепций – вложенный классы используются значительно реже, чем внешние. Человек не любит вложенных структур, он любит последовательные структуры. Объектов 20 уже тяжело удерживать в мозгу и их надо структурировать.
Модель вложенных пакетов является существенно более гибкой. Если вложенные пакеты отсутствуют, то это типичная одноуровневая модель.
Экспорт и импорт имён:
В языке Ада есть одно глобальное пространство имен, при этом считается, что оно как бы вложено внутрь модуля STANDARD. Любой программный пакет является погруженным в некий модуль STANDARD. Программа состоит из некоторой последовательности библиотечных пакетов, могут быть вложенные пакеты. Имя становится видно с момента его определения.
package P
P1
M P P.P1 ... Pn
M1
D
D1
end P;
P.P1 //все имена описанные в спецификации соответствующего пакета, они становятся потенциально видимыми в точке после
Непосредственно видимыми являются только имена соответствующих пакетов, все остальные имена являются потенциально видимыми. Если для Оберона этого было достаточно, то в Аде этого совсем не достаточно, так как в Аде допускается перекрытие операций, а именно перекрытие имен процедур и функций, при этом это перекрытие распространяется и на стандартные знаки операций.
package Vectors is
type Vector is ...;
function "+" (V1, V@: Vector) return Vector; // переопределение «+»
Утверждается, что если у нас есть только потенциальная видимость, то смысл в использовании этого перекрытого оператора «+» просто пропадает.
X1, X2, X3:Vector;
X3:= Vectors."+" (X1, X2); //Вся прелесть оператора "+" теряется.
Для этого есть конструкция, которая снимает потенциальную видимость, делая имена непосредственно видимыми:
use список_имён_пакетов;
В языках Модула-2 и Дельфи (с простой модульной структурой) import (uses) должны быть первыми операторами. В языке Ада модули могут быть вложены друг в друга, и от их порядка очень многое зависит. И предложение use может быть на любом уровне.
Пример:
package М
package М1
N
end М1;
1.//М1.N – видимо потенциально в данной точке
2. если use M1
package М2
N
end М2;
1.//М1. N и М2. N – видимо потенциально в данной точке
2. то видно N и М2. N
3. use M2 – видны имена и из М1 и из М2, и они между собой конфликтуют и имя N совсем не видно – в результате семантика программы может сильно поменяться
end М;
Итак, от добавления или перемещения use – семантика программы может весьма измениться. Во многих организациях использование оператора use было "законодательно" запрещено. Вместо него использовался другой:
имя1 renames имя2 // исправлял ситуации связанные с экспортом функций, которые перекрывали стандартные знаки операций
Безусловно, есть некоторые выгоды, которые можно получить за счет усложнения правил видимость и структуры языка, однако, как показывает практика, недостатки, которые появляются наряду с этими преимуществами – перевешивают.
В современных ЯП модульная структура значительно упрощена, пусть и в ущерб гибкости.
Вспомним, что мы рассматривали модули как возможность определения новых типов данных – так в модуле мы один раз описываем структуры данных, процедуры и функции, а далее импортируем и используем ранее описанные элементы.
п.3. Понятие класса
Класс как языковый конструкт служит для определения нового типа данных (ТД) и обобщает понятие записи (за счёт того, что класс объединяет в одно место не только данные, но процедуры и функции). Не даром во многих современных языках понятие записи или вообще отсутствует (Java), либо является частным случаем класса.
Член класса:
- Данные;
- Функции и процедуры;
- Другие типы данных (перечисления, вложенные классы и т.д.)
-
в языках Cи++, Java, Cи#:
class имя_класса [наследование] //возможно отсутствует
{
определение члена
}
-
В языке Дельфи:
type X= class [(наследование)] //возможно отсутствует
объявление членов
end;
1. Большинство современных ЯП используют объектно-ссылочную семантику (все объекты класса всегда являются ссылками) - Delphi, Cи#, Java.
Объектно-ориентированные языки, которые не имеют объектно-ссылочной семантики - Си++ и Оберон (формально).
Почему все объекты в языках располагаются в динамической памяти, практика программирования объектно-ориентированных программ показала, что в большинстве случаев предсказать время жизни объекта тяжело, правда иногда это может быть легко – пример с диалогами, когда время жизни диалога ограничено одним блоком. Но такие случаи редки. Таким образом, даже в Обероне, в котором формально объект не имеет ссылочной семантики, большинство объектов реально располагаются в динамической памяти. Мы существенно упрощаем работу я языком, если заставим все объекты по определению размещаться в динамической памяти. Следовательно, объекты в этих языках появляются тогда, когда мы применяем некоторую операцию порождения.
в Си# и Java
X x; // объявление неинициализированной ссылки, любое обращение к которой ведет к прерыванию
x = new X(); // порождение самого объекта в Си# и Java
в Дельфи
x: XType;
x = XType.Create(); // create – специальная функция член класса – конструктор
Все объекты (даже если мы при объявлении класса явно не указали, откуда он наследуется) прямо или косвенно наследуются из единого объекта Object для Cи#, TObject для Дельфи. string и object (именно с маленькой буквы) - ключевые слова языка Cи#, это базисные типы данных. String и Object - классы-обёртки для типов string и object соответственно (из common language runtime).
В Дельфи общий предок для всех типов данных - TObject, у которого есть метод Create, поэтому у большинства классов конструктор тоже называется create, хотя мы можем присваивать другие имена конструкторам. Конструктор, который создает объект, загружаемый с внешней памяти, называют – load.
Создание объекта в языке Java: имя класса.имя_конструктора(); - мы явно вызываем некоторую функцию, которая является конструктором, можем передавать ей некоторые параметры. Компилятор вставляет вызов к менеджеру динамической памяти, который отводит в динамической памяти кусок, вызывает для этого куска конструктор и присваивает объекту ссылку на этот кусок.
X:= ....
new X() - явный вызов конструктора.
Очевидный вопрос: кто уничтожает объекты?
В Паскале есть new и dispose, а языках Си#, Java и в Обероне применяется автоматическая сборка мусора. В таких языках используется техника mark_and_scan (см. в предыдущих лекциях):
-
Чем это хорошо: не надо явным образом вызывать оператор delete (с которым часто были ошибки).
-
Недостаток: в непредсказуемый момент времени программа может просто подвиснуть, запустив сборщик мусора.
Все эти языки специально поставляют некоторые пакеты, занимающиеся сборкой мусора, например, в Си# есть пакет GC (garbage collector), специальный класс, который отвечает за сборку мусора. Можно заставить его проработать в фиксированный момент времени, можно подавить вызов сборщика мусора, однако с точки зрения накладных расходов вызов GC – всегда достаточно «суровая» вещь. Писать программы, которые должны гарантировать отклик в реальном времени, на подобных языках весьма затруднительно.
Какая проблема порождается автоматической сборкой мусора (речь идет об автоматической утилизации только одного ресурса- динамической памяти): память - не единственный системный ресурс, который является критичным. Файлы, устройства ввода-вывода - критичные ресурсы. Любой класс захватывает не только динамическую память, но и кучу ресурсов. В какой момент освобождаются эти ресурсы? С динамической сборкой мусора мы не можем предсказать, когда освободится тот или иной ресурс. А если памяти достаточно на очень большое количество объектов? В языке Cи# и Java предусмотрены средства борьбы с данной проблемой. Си# есть специальный интерфейс IDisposable - метод IDispose вызывается для освобождения ресурсов, занимаемых данным объектом. Рекомендуется перед тем, как присвоить ссылке на объект NULL, выполнить метод IDispose (если объект поддерживает данный интерфейс и захватывает какие-нибудь объекты кроме динамической памяти). Вся это актуально для графических приложений – где иконки, кисти и пр. являются критичными ресурсами.
В Дельфи есть объектно-ссылочная модель, а никакой сборки мусора нет. И это несильно мешает программированию.
Какие операции связаны с классом: