Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
77
Добавлен:
02.05.2014
Размер:
102.91 Кб
Скачать

Лекция № 15

Одностороннее связывание, которое есть во всех ЯП. У нас есть модуль сервера, интерфейс, которого предлагает некоторый набор типов данных и имен переменных, и есть модуль клиента. Особенность его в том, что серверный модуль никак не зависит от клиентского, т.е. серверный модуль ничего не знает о клиентском. Интерфейсная часть серверного модуля импортируется клиентскому.

Сервер Клиент

Некоторая конструкция

импорта

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

В языках Модула-2, Оберон есть предложения Import, в языке Delphi есть предложение uses, и, наконец, язык Ада, который несколько расширяет понятие модуля тем, что в одном физическом модуле может образовываться совокупностью нескольких логических. Есть понятие «единицы компиляции». И если в более простых языках (Delphi, Оберон, Модула-2) единица компиляции тоже самое, что и библиотечный модуль, т. е. логическое понятие, то в Аде – это некоторая последовательность логически связанных модулей, но, все равно, предложения, типа выше указанных, остаются.

Ада WITH (список имен)

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

STANDART

ЕК1

ЕК2

…………..

ЕКN

В случае, когда речь идет о раздельной компиляции. Если вместо этих ЕК взять и поставить текст, то мы компилируем все за один раз в составе одного модуля STANDART. Имена из модуля STANDART импортировать не надо. Они предполагаются уже импортированными. В случае же физически раздельной компиляции считается, что виден только модуль STANDART. Чтобы стали видны остальные ЕК, нужен конструктор имен, который дает только потенциальную видимость, если мы хотим непосредственную видимость (все имена, конечно не конфликтующие, становятся видимы), конструкцию

Uses (список имен)

можно использовать в раздельной трансляции.

Мы уже говорили о том, что Ада позволяет нам двустороннее связывание. Зачем оно нужно? Ни в одном из ранее перечисленных языков это понятие не требовалось. Это по тому, что у нас отсутствовали вложенные модули. Ни в Delphi, ни в Модуле-2, ни в Обероне у нас один модуль в другой вкладываться не мог. В Аде другая ситуация. Например, у нас есть модульMAIN, внутри которого сидит модуль М1, М2 и еще какая-нибудь процедура Р.

MAIN

PROC P

М1

М2

С помощью одностороннего связывания это можно сказать, что у нас есть

MAIN М1 М2PROC P

В принципе, можно обойтись и обычным односторонним связыванием, но эта ситуация чем может быть нехороша? Например, модули М1 и М2 могут иметь смысл только в контексте модуля MAIN, равно как и процедура Р. Речь здесь идет только об интерфейсных частях. Естественно у модуля есть еще и тело. Т. е. у пакета есть идентификатор пакета и есть тело пакета (его реализация). Аналогичная ситуация и с процедурой (интерфейс процедуры есть ее прототип). При такой реализации модули М1 и М2 не имеют никакого отношения к реализации пакетаMAIN. Т.е. они должны осуществлять действия, которые не зависят от реализацииMAIN. А в случае вложенной структуры, у нас вложенные объекты должны иметь доступ ко всем ресурсам модуляMAIN, а не только к тому, что описано и не только в интерфейсной части. Т.е. это настоящий вложенный объект. Подобного рода вещи с помощью одностороннего связывания реализовать очень и очень тяжело. Такое проектирование называется «проектирование сверху вниз», и Ада один из языков, которые мы сейчас рассматриваем, позволяет нам реализовать такую гибкую схему. Синтаксически это можно реализовать так.

Есть package body is

(мы хотим, чтобы М1 и М2 представляли из себя внутренность модуляMAIN), и в тоже время мы хотим реализовать раздельную компиляцию. Для этого есть будем использовать двустороннее связывание. Есть так называемая заглушка и указание на контекст.

Package body MAIN is package body M1 is separate

package body M2 is separate

procedure P (X:INTEGER) is separate

end MAIN;

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

Separate (MAIN)

package body M1 is procedure PP is separate;

end MAIN;

separate (MAIN)

procedure P (X: INTEGER) is

………………

end P;

Аналогично все то же самое надо описать для М2.

Интересно, что вложенность может быть абсолютно любая. Т. е. внутри, например, модуля М1 мы можем описать еще одну своженную структуру.

separate (MAIN М1)

procedure PP is

………………

end PP;

Это получится вторичный модуль. Его отличие от первичного в том, что имена их указываются с именем первичного. Так вот внутри вторичного модуля в свою очередь могут быть другие вторичные модули. Чтобы нормально откомпилировать РР нужна полная информация о М1 и MAIN. Т.е. двустороннее связывание реализуется через заглушку и указание контекста. В Аде можно спроектировать цельную программу, а потом отдельные логические куски выдернуть и откомпилировать отдельно. Надо сказать, что другие ЯП с этой точки зрения до Ады еще не дотянули.

Рассмотрим механизм зависимой трансляции в языках, которые основаны на классах. Все ЯП, которые мы до этого рассматривали, включая и Аду, с точки зрения раздельной, зависимой трансляции, характеризуются тем, что вопрос о физическом нахождение файла (понятно, что модуль в ЯП – это есть некоторый текстовый файл, возникает вопрос, где они находятся) просто игнорировали. Считалось, что это задача системного программирования, и ЯП этого не касался. В этом есть свой смысл, потому что не хотелось зависеть от реализации какой-то конкретной файловой системы. Но сейчас любая общеупотребительная архитектура имеет иерархическую файловую систему. Имеется универсальный способ указания ресурса (текстовый файл, естественно, является ресурсом). Сегодня различия между файловыми системами минимальные. Если в Аде различия между физическим модулем и файлом игнорируются. Мы можем каждый логический модуль в файл поместить, а можем несколько логических модулей объединить в один физический файл, главное, что есть у нас понятие единицы компиляции. В Обероне , Delphiи Модула-2 один модуль – один файл. Создатели таких языков какJava и C# ориентировались на современные особенности файловых систем.И они решили немного обобщить эту схему. Программа наJavaпредставляет из себя некоторую совокупность приложений, если проект мало-мальски сложный, то у нас возникают сгруппированные ресурсы.

  1. Стандартные библиотеки, то, что поставляется обычно с ЯП, но не является частью самих языков.

  2. Сторонние компоненты, это то, что мы дополнительно покупаем, воруем или как-то еще достаем, т.е. приобретаем откуда-то извне.

  3. Свои компоненты

Все вышеперечисленное и образует законченный пакет. В результате создатели языкаJavaполучили следующее. Во-первых, возникает понятие пакета, если в Аде пакет – это некоторый логический модуль, то вJavaлогический модуль – это класс, в то же время совокупность классов формирует некоторый пакет. В результате, как разбиваются классы на файлы – это уже вопрос реализации. Самый простой способ:

1 класс = 1 файл

Этого обычно требуют все реализации Java, правда есть исключения, которые требуют немного поменьше, а именно, главный класс, где есть статическая функцияMain, должен быть в отдельном файле, имя этого файла должно совпадать с именем класса, и соответственно имя получающегося приложения будет иметь имя этого файла. А вспомогательные классы уже могут располагаться по-другому. Вот Вам и раздельная трансляция. Все классы группируются в некоторый пакет. Пакет – это совокупность классов. А как следствие – совокупность файлов. Так вот, если в предыдущих языках ничего не говорилось о том, где эти файлы должны располагаться, то пакет должен располагаться в том месте (на одном уровне файловой системы), где находится некоторая совокупность файлов. Предполагается, что все классы, которые входят в данное приложение хранятся в одной директории и образуют пакет. Наша задача, указать кокой пакет к чему относится. По этому первым объявлением в файле должно бытьPackage ‘имя объекта’. Пакет образован всеми файлами, которые приписывают себя к соответствующему пакету.

Например:

Rectangle.java

package Graphics; имя некоторой директории на некотором уровне файловой системы

public class Rectangle {

………….

};

Сами пакеты могут быть сложены в другие пакеты (не надо путать с Адой). Здесь аналогично тому, что папки в файловой системе могут быть вложены в другие папки, т.е. сами папки могут иметь иерархическую структуру.

Фирма SUNпредложила следующий момент. У нас есть понятие универсальная нотация того, где находится наш проект.

com.company.MyEditPji.Graphics

com.company.MyEditPji.InputOutput пакеты

Пакет имеет иерархическую структуру, но эта структура представляет связь приблизительно такую же, как любая команда разработчиков (есть главный большой шеф, есть лидер команды, под ним сидят несколько программистов). Это все не есть вложение ресурсов один в другой – здесь все отдельно. Логическая структура отражает не характер связи внутри проекта, а характер связи внутри бюрократии. Реальной вложенности здесь, конечно же, нет. Все эти пакеты ведут себя как односторонне связанные. Все ЯП, кроме Ада поддерживают исключительно односторонне связывание.

Как компилятор ведет себя с точки зрения раздельной трансляции? Есть некоторая переменная окружения, которая называется CLASS_PATH, в ней находятся директории, в которых вообще могут находиться файлы. На самом деле это список корневых директорий. Какое значение она принимает, зависит он конкретной операционной системы.

В Windows

D:\Java\CLASSES;

E:\MyProjects;

Это и есть две директории, с которых начинается поиск. Чтобы организовать импорт с односторонним связыванием, у нас есть конструкция

Import имя_пакета;

Import Graphics.*;

С точки зрения компиляции это означает, что компилятор берет все каталоги из переменной окружения, ищет там директорию Graphics и, если находит, просматривает все файлы которые там находятся. Они-то и образуют сам пакет. Все имена из этого пакета могут импортироваться. У нас есть две директивы импорта. Одна импортирует все имена, которые описаны в пакетеGraphics,при этом эти имена классов должны иметь атрибут public.

Import Graphics.Rectangle;

Это, если нас интересует только прямоугольник. Такая форма нужна для ускорения процесса компиляции, и получающийся файл будет иметь меньший размер, нежели в первом случае, потому что там компилятор будет импортировать все имена, которые есть в пакете, а во втором случае достаточно одного класса. Компилятор легко находит в соответствующей директории файл с именем Rectangle и из него извлекает необходимую информацию о классе. Более того, мы имеет право не писатьimport.Любое имя в языкеJavaимеет вид:

Имя х;

Определяем имя объекта х. Откуда должно браться «С»? Оно берется либо из имени текущего пакета (берется по умолчанию), либо оно должно быть описано в виде

Import M.*;

Если не хотим употреблять Import, то можем явно писать M.x;

Сами имена пакетов имеют точно такую же строчечную структуру. Следовательно, можно просто писать:

E:\Company

И пакет может выглядеть так

MyEditPji.Graphics.Rectangle ;

Компилятор видит, что это имя пакета, он пытается отыскать в нужной директории имя поддиректории Graphics, там он ищет файл Rectangle.Из этого файла он вытаскивает необходимую информацию. Интересно, что в случае языкаJavaинформация об импортируемых из данного пакета типах хранится вместе с самим классом. Это делается из соображений безопасности. Т.е. у нас Rectangle.java компилятор транслирует его в байт-код, который будет записан в файл с именем Rectangle.class. Представление байт-кода – это не только сам байт-код, инструкции виртуальнойJava-машине, но и вся интерфейсная информация. Вся информация предается вместе с программой. Это сделано из соображения безопасности. Когда какой-то класс загружается на машину, тут же идет контроль интерфейса. Т.е. мы не можем при ретрансляции обмануть виртуальнуюJava-машину поскольку информация о типах у нас находится вместе с программой. Получается, что явной конструкции импорта нет, но он осуществляется как бы неявно. Типичное одностороннее связывание. Серверный модуль, напримерGraphics, ничего не знает о модуле, который будет его импортировать. Несмотря на то, что у нас есть возможность вкладывания одного пакета в другой, речь идет о псевдо иерархической организации файла и нечего более. Здесь только одностороннее связывание. Механизм раздельной трансляции достаточно очевиден, на современных компьютерах поиск по каталогам осуществляется достаточно быстро. Расходами на трансляцию и ретрансляцию можно пренебречь. Поэтому такая схема с удовольствием принята большинством программистов.

Создатели языка С# сделали примерно то же самое, только назвали немного по-другому. Например, вместо понятия пакета у них присутствует понятие пространства имен. Точно так же, как вJava, каждый класс должен принадлежать некоторому пакету, при этом свою принадлежность он объявляет в первой команде файла.

Package имя_пакета

Имя пакета представляет из себя либо один идентификатор, либо совокупность идентификаторов, перечисленных через точку.

В С#каждое объявление имеет вид

Namespace имя{

.

.

(описание класса)

.

.

};

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

Namespace N1 {

Public class A {…};

Namespace N2 {

Public class B {…};

};

};

В Java тоже была возможность вкладывать один пакет в другой. Если мы хотим использовать то, что описали о водном файле, в другом. Это можно сделать так:

Namespace X {

N1. A x = new A ( );

N1.N2 B y = new B ( );

Так мы видим, что вложенность присутствует как и в Java, но только там она на уровне файловой системы, а здесь мы про это забываем. Здесь намного удобнее реализовано с точки зрения видимости. Если на Java пришлось бы писать что-то типа

N1. A c = new A ( ); и пакет N1пришлось бы импортировать.

Здесь достаточно будет написать:

A d = new A ( );

Прямо в описании языка сказано, что конструкция такого рода

Namespace N1 {

Namespace N2 {

Namespace N3 {

};

};

};

эквивалентна записи

namespace N1, N2, N3

но это то же самое, если бы мы в Java написали бы

package N1.N2.N3 {

……………….

};

связь между понятием пакета в Java и понятием пространства имен в С# довона очевидная. Точно так же как в Java есть импорт, так же он есть в С# как явный так и неявный, только здесь свой синтаксис (язык С# похож на С, ни в коем случае не на Java).

Using System.Console; серверный модуль

Можем так жк и переименновывать

Using Cons = System.Console;

Теперь везде, где компилятор увидит Cons, он будет понимать, что это на самом деле System.Console. Это сделано для возможности укорачивать запись. Для раздельной трансляции точно такие же, как в Java требования, чтобы все файлы находились в определенном месте файловой системы. Опять же подчеркнем, что связь исключительно односторонняя, т.е. серверный модуль едонообразен для всех клиентских. Реальной вложенности нет, она есть только в языке Ада.

Еще о раздельной трансляции можно сказать следующее. Изначально в 70-е – 80-е годы говорить об языке, пригодном для индустриального программирования, без раздельной трансляции не приходилось. Это требовалось, в первую очередь, для минимизации расходов на перетрансляцию. Например, в Модуле-2, если мы меняем реализацию, надо перекомпилировать только файл реализации, а все остальные клиентские модули не затрагиваются, поскольку не изменен интерфейс. Такая же схема реализована в языке Оберон и Delphi. В Аде немного другая ситуация.

Import имя_пакета

Package P is

Type T is private;

Private ….

Type T is ……;

End P;

Если мы меняем что-то в пакете Р, мы вынуждены перетранслировать не только пакет Р, но и все, что ссылается на пакет Р т. е. все клиентские модули для пакета Р. Потому что компилятор производит перераспределение памяти, одно это уже достаточное основан6ие для перетрансляции. Следовательно накладные расходы на перетрансляцию в языке Ада немного больше. С современной точки зрения расходы на перетрансляцию становятся все менее и менее значимые. В современных ЯП любое изменение пакета приводит к перетрансляции всех модулей, которые используют данный пакет. В С++, о котором мы не говорили, поскольку там независимая трансляция, Страуструп сказал, что, если Вы хотите минимизировать расходы на перетрансляцию, то нужно это делать через абстрактные типы данных.

Зависимая трансляция хороша тем, что она позволяет нам контролировать модульную целостность во время трансляции, а в языке Java и при выполнение (вся ниформация о типе данных сосредоточена в оттранслированном классе). Обмануть Java-машину практически невозможно.

Независимая трансляция. Там есть единица компиляции и есть транслятор. Контекст должен быть указан в самой единице компиляции. В языке С единственно возможный был способ

Extern ….. //функции, переменные

Типы данных надо было дублировать в каждой единице компиляции. Хорошим стилем программирования считается, что у нас есть модуль , и его интерфейс помещается в файл М.h, а реализация в файл М.с (срр). Теперь, если мы хотим использовать интерфейсы, мы должны воспользоваться механизмом#includeфайлов. Правила хорошего тона говорят, что если появляется включениеextern и, вообще, прототипы функций могут появиться только вinclude-файле. Это очень похоже на импорт, чем по сути и является. Другое дело, что это никак не контролируется ни языком, ни чем-то еще. Если у нас такая ситуация:

ММ1 М2

М2

Получится, что М2 появиться дважды в одной и той же единице трансляции. Поэтому хороший программист начинает include-файл с проверки.

#ifndef __My_F__H__

#define

#endef

/* do not add anything

Каждому include-файлу присваивается уникальное имя, некоторые среды генерируют его автоматически.

Еще одна проблема такова, что у нас крайне не структурированное пространство имен в языке С и С++. У нас есть спецификатор extern, который используется по умолчанию, который говорит, что данные имена является глобальными и видимыми везде. И есть спецификаторstatic, который говорит, что данное описание видимо только в пределах данного модуля. В других ЯП, если мы импортируем два модуля, и у нас возникает конфликт, мы его разрешаем так: М1.х и М2.х доже если х является явно видимым. В случаеinclude-файлов мы таким образом данный конфликт обойти не можем. Если одно имя использовано несколько раз, нам уже ничего не поможет. В языке С++, как и в С# (на самом деле: вC#, как в С++) есть понятие пространства имен (namespace).Отличие состоит в том, что пространство имен в С++ не является расширяемым. Это означает, что, если мы вJava, илиC#могли описать пакет (пространство имен), а потом ниже к нему еще что-то дописать, то здесь этого уже нельзя. В одном файле два пространства имен с одинаковыми именами появиться не может. Пространства имен могут быть вложенными, т.е.

Namespace N1 {

Namespace N2 {

Class x {…};

};

};

Технология осталась той же самой. Если все это находится в каком-то файле М, то мы должны сделать следующее:

Include < M >

N1::N2::x

Или вместо этого можем написать

Using namespace N1;

Интересно, что в С++ появилось так называемое неименнованное пространство имен, т.е. пространство имен без имени.

Namespace {

Class A{…};

Class B{…};

};

После этого можно написать

A a = new A ( );

Это эквивалентно тому, что когда компилятор видит не именованное пространство имен, поскольку компилятор видит только один файл, он генерирует уникальное имя, переписывает все это как

Соседние файлы в папке Лекции по программированию на ЯВУ