Лекции (1129116), страница 17
Текст из файла (страница 17)
constructor Init(…);
destructor Destroy(…);
end;
var X : T; //определение объекта
…
X = T.Init(…); // инициализация объекта
Все элементы классов размещаются в динамической памяти.
Еще одна интересная особенность, сближающая Delphi и Java, это то, что в Delphi есть некий класс, значениями которого, являются другие классы (класс классов). Т.е. мы можем порождать объекты непонятно какого класса (подробнее это будем обсуждать позднее).
Системной семантики у конструктора нет, т.е. это обычная функция, которая инициализирует объект. Если в конструкторе нужно вызвать конструктор базового класса, то это можно сделать в самом начале, с помощью ключевого слова inherited: inherited Create(…). В языке Delphi за программиста компилятор ничего в конструктор подставлять не будет.
Поскольку все объекты класса размещаются в динамической памяти, то требуется освобождать память. Поскольку динамической сборки мусора в Delphi нет, то освобождать память нужно явно, с помощью понятия деструктора. У деструкторов также нет никакой системной семантики. Кроме того, у класса TObject есть метод Free(), который вызывается так: x.Free(). Этот метод смотрит, равен ли x константе nil (аналог NULL), если равен, то не делает ничего, а если не равен, то вызывается деструктор, и освобождается память. Но Free() не присваивает объекту nil, об этом должен позаботится программист.
Аналогично как и в конструкторах, в деструкторе нужно вызывать деструкторы базовых классов. Разумеется, эти деструкторы надо вызывать в конце данного деструктора.
Язык Delphi нужен только для программирования под Windows, и вне этой системы он не имеет смысла.
Глава 5. Инкапсуляция. Абстрактные типы данных (АТД).
Инкапсуляция – это скрытие деталей реализации типа данных от доступа извне. Каждый тип данных представляется множеством значений и множеством операций. Инкапсуляция – это скрытие деталей структуры данных, и, возможно, некоторых операций. Скрытые детали реализации доступны только для операций данного типа (закрытый член класса можно использовать только из функций-членов этого класса).
Инкапсуляция нужна, прежде всего, для надежности (например, бесконтрольный доступ к элементам класса Stack – body и top, нарушает целостность структуры данных). Как ни странно, инкапсуляция не усложняет задачу клиента, а довольно часто наоборот упрощает. Она позволяет изолировать внимание программиста, который использует соответствующий класс, от несущественных, с точки зрения использования, деталей.
Модула–2.
Мы уже говорили о понятии логического и физического модуля в этом языке. Здесь самый простой подход к инкапсуляции:
DEFENITION MODULE M; //Модуль определений
…… //все, что описано в модуле определений
//видно всем.
END M.
IMPLEMENTATION MODULE M; //Модуль реализации
…… //все, что описано в модуле реализации
//не видно никому.
END M.
Для того, чтобы использовать этот модуль в другом модуле, в использующем модуле должно быть объявление IMPORT M. Но объекты из импортируемого модуля доступны только через квалификатор модуля: имя_модуля.имя_объекта. Поскольку модули друг в друга вкладывать нельзя, то хватает одного уровня уточнения (квалификации). Если нам тяжело каждый раз писать квалификатор, то мы можем написать другую форму импортирования: FROM имя_модуля IMPOPT список_имен_объектов. После этого, квалификатор можно не писать, но за это приходится выписывать все используемые объекты вначале программы.
Как только речь заходит об импорте, то сразу возникает проблема конфликта имен. В двух импортируемых модулях могут быть объекты с одинаковыми именами, и в этом случае без квалификаторов обойтись нельзя. Если два разных объекта с одним именем, но из разных модулей импортируются с использованием FROM, то компилятор выдаст сообщение об ошибке, потому что возникает неоднозначность.
Delphi.
В языке Turbo Pascal и в Delphi все несколько проще. Импорт производится так: uses имена_модулей. Имена объектов из этих модулей можно использовать без квалификатора. При конфликте имен приоритет отдается локальному имени по отношению к импортируемому. Квалификатор нужен только тогда, когда возникает конфликт импортируемых имен.
Какой подход удобнее – более жесткий подход Модулы-2, или языка Turbo Pascal? На первый взгляд, подход Turbo Pascal удобнее, но когда приходится разбираться в больших программах, то при таком подходе нередко приходится смотреть реализацию исходников библиотек. Представьте, что вам встретилась некая переменная DelayFactor. Что она означает? Вы ищете в Help, и естественно ничего не находите. Как узнать к какому модулю она принадлежит, если в эту программу импортируется десяток модулей? Не случайно есть утилита grep, которая позволяет искать строку в заданном наборе файлов – это совершенно необходимая вещь. В Модуле-2 наличие квалификаторов в таких случаях очень помогает – сразу видно, к какому модулю принадлежит объект, а если квалификатора нет, то это можно посмотреть в заголовке IMPORT.
Т.е. программу на Модуле-2 гораздо удобнее читать. Кто важнее – читатель или писатель? Настоящий программист должен быть и тем и другим, тем более, что после серьезной работы над программным проектом, программист из писателя наполовину превращается в читателя.
Оберон.
Оберон – значительно более простой язык, чем Модула-2. В Обероне подход совсем жесткий: есть конструкция IMPORT список_имен_модулей. Программист всегда обязан обращаться к объектам через квалификатор: имя_модуля.имя_объекта. Конечно, писать приходится много (хотя можно использовать копирование), но зато читать совсем удобно.
Итак, в Обероне и Модуле-2 можно отметить следующие общие моменты:
-
Простота модульной структуры. Все модули вытягиваются в цепочку. Логически, конечно, у них есть некоторая иерархия (дерево), которая определяется отношением импорта, но взаимоотношения между модулями очень просты.
-
Инкапсуляция является наиболее простой – все что видно в модуле определений, то является общедоступным (через квалификатор). Но в Модуле-2 мы не можем скрыть часть структуры, скрыть можно только целую сущность. {* Оберон рассматривается на следующей лекции *}
Лекция 14
Инициализация статических членов в Java (отступление)
Когда мы говорили о языке Java, то забыли рассмотреть один момент, а именно – Инициализацию статических членов.
Статические члены (как функции, так и данные) играют роль глобальных функций и переменных, однако, не обладающие их недостатками. Язык Java вообще не принимает концепцию глобальных объектов – их роль играют статические члены.
Коль скоро статические члены в Java используются достаточно интенсивно, встает вопрос об их инициализации. В C++ этот вопрос практически не решен (нет механизмов). Проблема заключается в том, что статические объекты должны инициализироваться до входа в функцию main().
В Java этот вопрос решается, например, таким образом:
class X {
static i=10;
int j=1;
… }
Понятно, что код “static i=10” будет сгенерирован внутри конструктора, то есть компилятор генерирует конструктор таким образом, что в начале выполняется инициализация базового класса, потом инициализация, указанная в классе, а уж затем – тело конструктора. В случае, если у нас есть инициализация статических членов, то компилятор вставляет это в стандартный пролог, который выполняется перед выполнением функции main() соответствующего апплета.
Всегда возникает ситуация менее тривиальной инициализации, например:
static int[ ] arz = new int [10];
Но, вообще говоря, члены этого массива могут инициализироваться и по более хитрому правилу (например, им может быть присвоена совокупность 10 случайных чисел). Создатели Java подошли к этому вопросу серьезно, они разрешили создание статических блоков:
static { … };
Внутри блока может быть любая нетривиальная инициализация, вся она будет вставлена компилятором в стандартный пролог.
Еще в Java есть такая необычная возможность, как динамическая подгрузка классов. То есть есть специальный интерфейс, библиотеки классов, которые позволяют во время работы программы производить динамический поиск классов, по каким-то локаторам ресурсов, в том числе в удаленных местах. Очевидно, что инициализация статических членов этих классов будет выполняться динамически во время загрузки.
Pascal, Oberon, Modula-2, Ada, Delphi.
Когда мы говорили о модулях в таких языках, как Pascal, Oberon, Modula-2, то упоминали про инициализирующую часть, например, в Ada:
package body P is
…
begin
<инициализация>
end
- инициализация выполняется один раз при загрузке пакета. Так как пакет загружается статически, то инициализация находится в прологе программы.
Точно такая же возможность есть в модулях Modula-2 и Delphi. В последнем даже возможно делать так:
initialization
…
finalization
…
end имя_unit
На этом мы завершим разговор об инициализации статических членов и вернемся к уже начатой теме инкапсуляции и абстрактных типов данных.
Глава 5. Инкапсуляция. Абстрактные типы данных (АТД) (продолжение).
Modula-2
Мы говорили о том, что в Modula-2 инкапсулируются (скрываются от доступа извне) только данные и операции, находящиеся внутри модуля реализации. В случае же, если мы напишем:
DEFENITION MODULE M;
…
TYPE T=RECORD
…
END M;
, то у нас нет никакой возможности упрятать детали структуры T. Где же тогда инкапсуляция? В данном случае ее нет, так как единицей защиты служит целый тип, мы не можем ограничить доступ к отдельным данным этого типа. Возникает вопрос, а есть ли настоящая инкапсуляция в Modula-2? Есть.
Дело в том, что абстрактный тип данных с точки зрения пользователя выглядит только, как множество операций, структура данных (множество значений) пользователю не видна. Вобщем-то это и есть определение АТД. (Заметим, что АТД и абстрактный класс в C++ немного разные вещи и смешивать их не стоит).
Набор операций, открытый пользователю (любому объекту, использующему данный тип данных), называется интерфейсом АТД. Иначе говоря, доступ к АТД может осуществляться только через определенный интерфейс, никакие детали реализации недоступны.
Как описать такой абстрактный тип данных, например, на Modula-2? Заметим, что Modula-2 предлагает две альтернативы: полностью открыть структуру типа данных, либо полностью ее закрыть. В Modula-2 есть понятие скрытого типа – это тип, объявление которого в модуле определений выглядит, как
TYPE T;
- без структуры. Так как клиенту доступно только то, что описано в модуле определений, то к типу T никакие операции, кроме описанных там же в модуле определений не применимы. (Это не совсем так - кроме описанных операций еще можно использовать “:=”, “=”, “<>” – присваивание, равенство, неравенство.)
На первый взгляд, кажется, у нас появился АТД. Но проблема Modula-2 заключается в том, что в нем слишком жестко реализована раздельная трансляция. Мы будем говорить о ней позже, но сейчас отметим то, что модули реализации и определений должны быть физически разделены друг от друга – они компилируются отдельно. В принципе, можно даже откомпилировать их отдельно, а можно откомпилировать только модули определений и собрать программу, разумеется создать исполняемый файл мы не сможем.
В языке Ada такая же возможность – компилировать клиентские пакеты только при наличие интерфейса. В результате получается удобный механизм разработки и минимизации времени компиляции.
Заметим, что Turbo Pascal избрал другой подход: там определения и реализация соединены в одном модуле (Unit), не в последнюю очередь это было сделано из-за того, что Turbo Pascal невероятно быстро компилирует код. Это же унаследовала система Delphi.
О компиляции мы еще будем говорить позже. А сейчас отметим тот факт, что для реализации вышеуказанной схемы разделения модулей необходимо наложение на тип данных. Пусть у нас есть модуль M (в нем определен тип T) и клиентский модуль C.
Module C:
IMPORT M;
VAR X: M.T;
Как компилятор будет распределять память? Что он знает о типе данных T? Ответ: он ничего не знает, кроме имени. Отсюда вытекает, что на данную схему должны быть наложены ограничения, а именно – тип T ( с точки зрения реализации ) может быть либо указателем, либо типом, совместимым по размеру памяти с указателем (например, integer). Отсюда вытекает, что инкапсуляция в Modula-2 заставляет прибегать к реализации типа в динамической памяти. (Есть исключения: например, работа с файлами.)
Например, если у нас есть АТД стек:
DEFENITION module Stacks:
TYPE STACK;
PROCEDURE Init( VAR S: STACK );
PROCEDURE Push( …);
PROCEDURE Pop( … );
PROCEDURE Destroy( … );
END STACK;