лекции (2003) (Глазкова) (1160821), страница 20
Текст из файла (страница 20)
Мы ничего не знаем про структуру этого типа; более того, программисту знать структуру файла и не надо (она, конечно же, описана в файле stdio.h).
Файл определяется не своими свойствами как структура данных, а прежде всего набором операций.
С этой точки зрения, язык Си можно назвать развиваемым языком (средства развития есть). В чем недостаток такого определения файла в языке Си? В языке Си нет хороших средств защиты.
Для того, чтобы средство определения новых типов было более адекватным, необходим специальный контейнер, который позволил бы объединить множество операций и множество значений и предоставить все это вместе как законченный тип данных. Любой контейнер будем называть логическим модулем.
Проблема языков типа Си такова, что в них концепция логического модуля не доведена до конца. Ее можно заменить некой совокупностью соглашений (например, что у физического файла должен быть интерфейс, что интерфейс мы заключаем в файлы .h и что эти файлы надо включать с помощью директивы include, причем вначале).
Необходимо более мощное понятие логического модуля. Минимальным логическим модулем является процедура. Но одних процедур недостаточно. Необходима конструкция более высокого уровня, которая позволяет сгруппировать процедуры.
ЯП делятся на две большие группы:
-
языки, в которых вводится понятие модуля (unit package);
-
языки, основанные на понятии класса.
Заметим, что и понятие модуля, и класса не сводятся только к типам данным. Модуль – очень мощное понятие. В частности, его можно приспособить, чтобы определять типы данных. Класс изначально ориентирован на то, что это тип данных
модульные ЯП | ЯП, основанные на понятии класса |
Ada Modula Oberon Delphi | C++ Java C# Delphi |
Понятие модуля используется и для разбиения программы на части, и для раздельной трансляции, и для определения новых ТД. В модульных языках понятие модуля очень важно и даже несколько перегружено по функциональности.
Класс - это ТД, который определяется пользователем. В современных ЯП те типы, которые даже не определяются пользователем, выглядят как классы. Концепция класса очень удобна.
Язык Delphi развивался на основе традиционного ЯП Turbo Pascal. Язык Turbo Pascal имел модульную структуру, которая осталась и в языке Delphi. В более поздних версиях языка Delphi есть и понятие класса.
Заметим, что и в языках, основанных на понятии класса, тоже есть понятие модуля, т.к. кроме разбиения программы на логические модули, необходимо ее разбиение на физические модули.
Пункт 2.
Модули и типы данных. Принципы раздельного определения, реализации и использования (РОРИ).
Рассмотрение начнем с языка Delphi. Программа на языке Delphi делится на модули, которые называются unit. В программе есть главный unit, который называется program и имеет следующий синтаксис:
program имя;
объявления
begin
операторы
end имя
Это главный модуль, а вся остальная часть программы состоит из библиотечных модулей:
unit имя;
интерфейс
реализация
end имя
Т.о., у нас есть некоторый контейнер. Для того, чтобы определить некоторый тип данных (например, Stack), мы должны определить его структуру - тип данных stack:
type Stack=record
top: integer
body: array [1..50] of char
end;
и набор процедур над ним (Push, Pop, IsEmpty):
procedure Push (var: s: stack, x: char);
Unit состоит из 2 частей:
-
интерфейсная часть:
Interface
набор объявлений (определение ТД и спецификация
(прототипы) операций, работающих с этим ТД)
Интерфейс - это то, что будет видимо в других библиотечных модулях и в главной программе.
-
реализация:
Implementation
набор объявлений
полный код операций
………………
То, что объявлено в реализации, нигде кроме самой реализации недоступно.
Также в реализации может быть часть:
begin
операторы
Эти операторы выполняются тогда, когда соответствующий модуль загружается в память. Эти операторы необходимы и нужны только для инициализации объектов данных, описанных в интерфейсе или реализации.
Свойство языка Delphi: весь unit должен находиться в одном файле, т.е. на физическом уровне определение и реализация совмещены, но на логическом уровне они разделены.
Принцип РОРИ - отдельно определение и реализация. В модульных ЯП определение и реализация разделены. Использование же всегда отделено от определения и реализации.
Именно поэтому на этих ЯП можно создать программный продукт, который можно использовать в отчужденном от автора виде.
Т.о., вопрос об использовании тесно связан с вопросом об организации среды ссылок. Язык Delphi дает достаточно простую и мощную модель. Все среды ссылок в языке Delphi состоят из:
-
предопределенной среды ссылок (предопределенные имена - char, integer);
-
глобальной среды ссылок (это имена модулей (unit) и имена из interface);
-
локальной среды ссылок.
Фактически, интерфейс определяет некоторый экспорт имен.
предопределенные имена | |||
М1 | М2 | ... | Мn |
Имена модулей
Локальная среда (среда ссылок реализации)
В Delphi допускается вложенность процедур, а, следовательно, и вложенность объявлений. Это скажется только на реализации.
Язык Delphi предлагает достаточно простой способ структуризации.
Возникают понятия: непосредственная видимость имени и потенциальная видимость.
Имена из глобальной среды ссылок видимы, но как?
Непосредственная видимость означает, что имя характеризует само себя без всякой дополнительной информации. Например, запись extern int a; на языке Си означает, что имя а видимо непосредственно (после этого можно писать x=a;). Более того, никакого другого способа кроме непосредственной видимости глобального имени в языке Си не существует.
Потенциальная видимость включает указание дополнительной информации, по которой можно добраться до этого имени. В языке Delphi потенциальная видимость имени означает доступность имени через имя соответствующего модуля (такой процесс называется квалификация имени):
имя модуля. имя
В программе на языке Delphi непосредственно видимы имена из предопределенной среды ссылок и имена модулей. Все остальные имена (имена из интерфейсов) видимы потенциально и, чтобы их использовать, необходимо писать:
uses список_имен_модулей;
Это предложение импорта. После этого предложения все имена, определенные в указанных модулях, становятся видимы непосредственно.
Заметим, что внешне это очень похоже на стиль программирования на языке Си. Каждую программу разбиваем на .h файл и .с файл, а вместо предложения импорта используем директиву #include. В чем недостаток Си? При конфликте имен мы ничего сделать не можем (только поменять имена).
Пусть имя Х определено и в модуле М1 и в модуле М2. С помощью механизма квалификации языка Delphi, мы можем легко различать эти имена: М1.Х и М2.Х.
Если возникает конфликт имен, то он решается следующим образом: имя из своей среды всегда пользуется приоритетом (т.е. в нашем модуле мы можем переопределять имена, как из предопределенной среды ссылок, так и из глобальной среды ссылок). А если же имена равноправные (импорт имен из разных глобальных сред), и они конфликтуют, то эти имена перестают быть непосредственно видимы, но могут быть видимы потенциально.
Т.о., мы имеем раздельное определение, реализацию и использование.
Чем удобно это разделение?
Во-первых, с точки зрения программирования, для того, чтобы проанализировать экспорт соответствующего модуля, нужно анализировать только его заголовок (только интерфейс), и не надо разбираться в деталях (мы отвлекаемся от проблем, связанных с реализацией).
Во-вторых, независимость от реализации приводит к тому, что если нас не устраивает конкретный вариант реализации, мы можем ее заменить, и при этом не переделывать использование.
Мы обсуждали, что самая большая проблема в индустриальном программировании - это проблема сопровождения. Сопровождение - это процесс модификации работающей программы (либо с целью исправления ошибок, либо с целью внесения новой функциональности, либо с целью адаптации программы к новым условиям (например, к изменившейся аппаратуре)). Для модификации программы надо поменять реализацию и перетранслировать всего один модуль.
Принцип РОРИ является вариантом схемы, предложенной в языке Modula 2 .
Это решало одновременно и вопросы раздельной трансляции, и видимости внутри программ; т.е. в каждом unit выделялась интерфейсная часть, которая образовывала глобальную среду ссылок
Для подключения раздельно транслируемых модулей надо использовать
uses M1, ..., Mn;
Какие иногда возникают проблемы?
Список uses может отсутствовать (но не в индустриальных программах).
Средняя длина списка имен модулей порядка 10. Как по имени Х отыскать модуль, в котором определено это имя? Имя Х может быть определено внутри процедуры, внутри модуля, в какой-то из локальных сред, среди предопределенных имен, в глобальной среде.
Как определить в каком из модулей находится имя?
При помощи средств разработки. Утилита Turbo Pascal gref позволяет искать по заданному шаблону некоторый текст в совокупности файлов. Обязательно в состав каждой среды разработки входит что-то вроде инспектора объектов (браузер объектов).
Вопрос определения, к какому модулю относится имя из списка, имен достаточно затруднительный.
В языке Modula 2 была предпринята другая попытка реализовать ту же схему. В Modula 2 каждый библиотечный модуль состоял из двух частей:
-
модуля определения (.DEF - файл) - интерфейс
DEFENITION MODULE M;
объявления
END M
-
модуля реализации (.MOD-файл)
IMPLEMENTATION MODULE M;
полный код процедур из .DEF
[BEGIN
операторы]
END M
Отличие Delphi от Modula 2: в Delphi это все объединяется в один unit, а в Modula 2 интерфейс и реализация были в разных файлах.
Чем удобней расположение в различных файлах?
Чтобы программа на Modula 2 прошла трансляцию, нужна только совокупность модулей определений. На этапе редактирования связей уже понадобятся .MOD-файлы. .MOD-файлы транслируются в .obj файлы, а .DEF-файлы транслируются в .SYM-файлы, которые представляют собой скомпилированную таблицу имен. В результате, если мы вносим изменения в .MOD-файл, надо откомпилировать только этот файл и после этого запустить редактор связей.
Рассмотрим аналогичную ситуацию в языке Си.
В C/C++ генерируется файл stdafx.h, который включается в каждый файл, и любое изменение в этом файле влечет перекомпиляцию всего проекта.
В Turbo Pascal и Modula 2 перекомпиляции не происходит при незначительных изменениях (например, при изменении комментария). Это достигается следующим образом. У нас есть новая и старая версия файла; компилятор перетранслирует модуль определений и смотрит, изменилась ли таблица имен (побайтовым сравнением). Если таблица имен не изменилась, то компилятор оставляет все как есть. Т.о., компилятор может применять достаточно гибкую схему управления перетрансляцией.
Итак, незначительное отличие Modula 2 от Delphi в обязательном физическом разделении определения и реализации.
Структура среды ссылок в Modula 2 такая же, как в Delphi.
предопределенная среда ссылок | |||
М1 | М2 | ... | Мn |
Имена модулей
Локальная среда ссылок
В Modula 2 есть два вида конструкции импорта:
-
IMPORT M1,...,Mn;
Mi - имена модулей.
Эта конструкция делает потенциально видимыми соответствующие имена из модулей. (Пример:
IMPORT InOut;
InOut.WriteInt();)
Эта конструкция импорта просто говорит компилятору загрузить таблицу имен для модулей М1,...,Mn.
-
Другая форма конструкции импорта:
FROM имя_модуля.
IMPORT список_имен