лекции (2004) (1160823), страница 12
Текст из файла (страница 12)
В) Интересная ситуация с языком Cи#, который с одной стороны претендует на роль чистого объектно-ориентированного языка (J# - расширение языка Java под .NET, и если сравнивать J# и Cи#, то Cи# гораздо ближе к Java, чем к Си++). Cи# гораздо шире по своим возможностям, не за счет новых концепций, а потому что расширена область применения языка. Так в Cи# есть указатели, но они могут применяться только в специальных блоках. В есть unsafe-блоки, в которых мы можем делать небезопасный код, очень похожий на Си++. Такие блоки должны быть подписаны, и пользователь может отключить их. Так, например:
Unsafe
{
char *m = malloc (10);
}
Понятие функционального типа данных оказалось достаточно удобным, появилась специальная концепция делегатов - обладает концепциями функционального типа. Обладает многими достоинствами функционального тд, в то же время не обладая его недостатками. Введено понятие делегата, внешне объявление делегата очень похоже на объявление функционального типа и в Обероне, и в Модуле 2, и в Аде 95. Ключевое слово delegate, за которым идёт спецификация подпрограммы.
delegate void Handler (Object o, Args e); // Handler - имя функционального типа.
В Cи#, как и в языке Java, нет просто функций. Есть понятие пространства имен, внутри этого пространства могут быть только объявления классов и перечислимых типов. Нет понятий глобальных функций или глобальных переменных. Если речь идет о функции, то сразу определяется к какому классу она принадлежит.
-
Если есть объявление Handler P; то P - делегат (функция с двумя указанными выше параметрами Object o, Args e). Вызывать её можно как обычно:
string S;
Args t;
P(S, t); // поскольку любой объект может быть преобразован к класс object, то это здесь и произойдет с помощью специальной процедуры boxing
-
К делегату применимы операции:
- круглые скобки (вызов функции)
- присваивание (P = F), внешне делегат выглядит как функция;
void F(Object o, Args x);
void G(Object o, Args x);
P = F; // такое присваивание инициализирует делегат, если попытаемся вызвать неинициализированный делегат, то произойдет системная ошибка
- "+=" и "-=": P+=G - делегат может иметь в себе несколько объектов функционального типа. P-=F; (добавление и исключение функций из списка). Вызов делегата в таком случае – это поочередной вызов зарегистрированных функций. Порядок вызова не фиксирован.
Чем делегат удобней, чем указатели на члены:
1. Делегат может держать несколько функций.
2. Делегат - конструкция безопасная. Никакого отношения к понятию указателя делегат не имеет.
3. обговаривалось, что в Си# все функции являются функциями-членами, однако при P=F, ничего не говорилось о том, что собой представляет функция F. Присваивание P=F не совсем верно. Надо сначала инициализировать:
P = new Handler(F); // (только для статических функций-членов класса)
P = new Handler (X.F); // Где: X - ссылка на целевой объект (target object); F - имя соответствующей функции;
А в делегатском списке каждый элемент – это ссылка на целевой объект и ссылки на соответствующую функцию. Таким образом делегаты похожи на указатели на члены из Си++, но все-таки делегаты шире. Потому что указатель на член – может быть только указателем на член данного класса, а здесь объектами одного и того же делегата могут быть любые функции, удовлетворяющие прототипу соответствующего хедера. А в случае указателей – указатель на статический объект будет нулевым, и компилятор это определит. Имя_объекта.имя_функции – это ссылка на объект запоминается как целевой объект, и соответственно ссылка на метод, компилятор проверит совпадает ли метод с сигнатурой делегата.
Все недостатки классического функционального типа, как указателя преодалены, и в то же время очень большая эффективность.
Частным случаем делегатов являются события, о которых мы будем говорить позже.
Переменный список аргументов.
Уже в языке С++ появился переменный список аргументов, например:
int printf(const char* fmt, ...); // Функция обязана иметь хотя бы один явный аргумент.
Для использования списка параметров дополнительно были введены специальные макросы: va_start, va_list, va_end, которые позволяют получить доступ поочередно к каждому параметру из списка, этот параметр считается совместим с int или с любым указательным типом, например void *. Берем и преобразуем к нужному типу, при этом никакого контроля нет, эта возможность признавалась одной из самых опасных в Си и Си++. Раз она опасна, то ее следует изничтожить.
В Паскале так же были переменные списки параметров, но только в стандартных процедурах. Например, writeln (x,y,z); // верно
writeln; // верно
Именно с точки зрения ввода-вывода переменный список параметров очень удобен.
Пример: на Си
printf("Count = %d\n", cnt);
на языке Оберон то же самое:
InOut.WriteString("Count = ");
InOut.WriteInt(cnt)
InOut.Writeln;
И это только для одного параметра, а если их будет больше? Кроме того проблема читабельности текста программы.
Концепция объектной ориентации может довольно элегантно решить эту проблему. Так в Си# ее решили достаточно просто. Напомним, что любой класс языка наследуется из класса Object, а любого не-класса (структура, или объект простого тд) существуют классы обертки.
Console.Write("Count = {0}\n", Cnt);// это метод имеет переменное число параметров
Процедура write имеет прототип:
void Write(String fmt, params object[] args); {0} - индекс элемента в списке параметров, индексы в общем случае могут быть вычисляемы
params – ключевое слово, когда компилятор видит это слово, первый параметр отождествляет с форматом, а из переменного списка параметров делает массив. У массива есть свойство длина – сразу можно вычислить. Как write догадывается, что нужно выводить целое число – по типу. В современных объектно-ориентированных языках есть динамическая идентификация типа, у нас есть надежное и контролируемое преобразование из ссылки object к любому конкретному типу Т. Могут быть ошибки, но эти ошибки будут отловлены именно в момент их возникновения, у нас не произойдет соответствующего преобразования типов.
Общий вид:
params T[] args - соответствует любой список параметров от пустого. Компилятор пытается переменный список объектов свести к однотипным объектам, тем которые указаны в объявлении, а ключевое слово params говорит о том, что отождествляется произвольный список, если его убрать то void Write(String fmt, Т[] args); - всего два параметра, второй – это массив (уже не компилятор, а сам программист его формирует). Такое решение может привести к дополнительным накладным расходам, но является надежным.
Глава 4: Определение новых типов данных
Определение: Тип данных - это множество значений (определяется самой структурой типа) плюс множество операций (определяет поведение объекта данного типа). Тип данных характеризуется прежде всего множеством операций.
Пример1: множество значений не есть однозначное множество
А)Типы int и unsigned int в языках Си и Си++ одинаковое множество значений, по крайней мере, с точки зрения реализации, но разное множество операций.
Б)В Си# byte: 0-255, sbyte: -128-127 - с точки зрения реализации множество значений одинакова, но разная интерпретация этого множества значений, а именно с точки зрения множества операций и семантики операций типы и отличаются.
Пример 2:
record
body: array [1..50] of T;
t: integer;
end.
Что это за тип – может быть что угодно, однако мы можем сказать, что это стек (где Т – верхушка, body – тело – хотя этого можно и не говорить), и применимы операции:
Push(X: T);
T Pop();
IsEmpty();
И сказать, что если применить push, а затем сразу pop то вернется Х. Теперь понятно какой тип данных мы ввели.
Средства для объявления новых тд появились уже давно, с современной точки зрения, в языках должны быть средства абстракции и средства защиты этой абстракции. В данном случае одно из средств защиты состоит из того, что нам необходим некий контейнер, который объединяет в себе как структуру данных соответствующую и связанные с ней объявления, так и множество операций. Рассмотрим каким образом реализуются эти контейнеры.
Все ЯП делятся на две основные группы:
1. Модульные языки (Модула-2, Оберон, Дельфи, Ада) - существует концепция Module, Unit, Package - логический модуль, который представляет собой коллекция ресурсов;
2. ЯП с классами - есть понятие класс, с одной стороны является обёрткой (сводит множество значений и множество операций), но ещё он является и типом (дуальность классов).
Понятие объектной ориентированности не сводится к понятию класса (обертка и тип одновременно), например, Ада 95 - модульный язык, но нет отдельного понятия класса (как обёртки), хотя есть ключевое слово class.
Модульные языки.
п.1. Понятие логического модуля
Трудно дать определение модуля, никакой четкой синтаксической структуры у модуля в разных яп нет. Модуль – это средство группировки, у модуля есть начало – заголовок, конец и внутреннее содержание. Только с точки зрения синтаксической структуры на модуль смотреть нельзя. Пример: рассмотрим условный оператор во многих яп,
if E then
S1
Else
S2
Endif
Тут тоже есть начало, конец и некоторое содержание, но модулем отдельные операторы никто не называет. Вот процедуру еще можно назвать модулем, пакет языка Ада точно можно назвать модулем.
Логический модуль (ЛМ) – то, что проще заимствовать, чем разрабатывать заново (неформальное определение из книги Кауфмана). С этой точки зрения при заимствовании операторов из кола мы их все-таки меняем («метод copy-paste» не рассматривается ), заимствование процедур, а особенно модулей, классов – распространено. Такие заимствования обычно не изменяются.
Итак модуль:
-
средство группировки ресурсов
-
механизм заимствования - для модуля всегда возникает понятие экспорта и импорта
При рассмотрении модуля как средства группировки – слово «логический» означает, то что мы абстрагируемся от разделения программы на физические модули. Физический модуль (ФМ) - понятие отличное от логического модуля. В Java ФМ - файл, ЛМ - класс и пакет. В Дельфи - ФМ и ЛМ совпадают, каждый unit находится в отдельном файле, и каждый фаил не может содержать больше одного unit’а. О взаимосвязях между логическими и физическими модулями будем еще говорит в главе посвященной раздельной компиляции. Сейчас речь пойдет только о ЛМ.
Рассмотрим простейший случай ЛМ, реализованный в Модуле-2 (похожее в Дельфи и Обероне).
1. Модули в Модуле-2 - очень важные понятия. Есть один главный модуль, и есть библиотечные модули (модули определений и модули реализаций). В модули определений содержатся только объявления структур данных и объявление операций, в модули реализаций - тела методов, вспомогательные процедуры, которые нужны только для реализации основных.
-
Главный модуль:
MODULE name;
.... объявления
BEGIN
операторы
END name.
-
Модули определения:
DEFINITION MODULE имя;
Только объявления
END имя.
-
Модуль реализации:
IMPLEMENTATION MODULE имя;
Для модуля реализации обязательно должен быть соответствующий модель объявлений, но не наоборот. Модуль реализаций не нужен, если например, в модуле определений нет ни одной процедуры. Все объявленные подпрограммы должны быть реализованы. Модуль реализации выступает как подчиненный модуль. Модуль определений содержит в себе только то, что нужно для использования. Тут мы уже видим понятие, с которым еще столкнемся позже - инкапсуляция (скрытие). Одна из главных целей появления логического модуля - понятие инкапсуляции. Такая структура скрывает от нас детали реализации.
Пример:
Модуль определений
DEFINITION MODULE Stacks;
CONST N = 50;
TYPE Stack = RECORD
body: ARRAY [1..N] OF Integer;
top: Integer;
END;
PROCEDURE Push (VARS: Stack; X:INTEGER);
PROCEDURE Pop (VAR S: Stack):Integer;
PROCEDURE Init (VAR S: Stack); // на что будет тор указывать
.............
VAR Done: Boolean; // true, если последняя операция успешна, false иначе
END Stacks;
Модуль реализаций
IMPLEMENTATION MODULE Stacks;
...