лекция 11 (Языки программирования (лекции) (2008))
Описание файла
Файл "лекция 11" внутри архива находится в папке "Языки программирования (лекции) (2008)". Документ из архива "Языки программирования (лекции) (2008)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "лекция 11"
Текст из документа "лекция 11"
Языки программирования.
Лекция 11.
Функциональный тд (продолжение)
В прошлый раз мы начали рассматривать конкретную реализацию концепции процедурных и функциональных тд в разных яп (Модула 2, Оберон и Ада).
1. Ада
А) В языке Ада 83 подпрограммного типа (т.е. функционального типа) не было вообще. Когда возникает потребность передать функцию, как параметр. Рассмотрим пример функции интегрирования Integral(F, ...). очевидно, что один из параметров – это интегрируемая функция. Это типичный случай, когда в качестве параметра мы передаем значений конкретной функции, которая где-то описана – т.е. это константа функционального типа.
Введение нового процедурного (подпрограммного типа) связано с тем фактически в таких языках, как Модула 2, Оберон и Си, и Паскаль то, что подразумевалось под подпрограммным типом - есть указатель на конкретную функцию. А как мы говорили указатель вещь весьма ненадежная. Поэтому говорить о том, что язык в котором есть указатели на функции – надежный не приходится, поэтому в Аде 83 не было, вместо этого используется концепция родовых сегментов (аналогичная шаблонам в Си++). Проблема передачи априори известной статической функции решается за счет статической параметризации.
Б) На языке Ада 95 появился подпрограммный тип данных:
type Integrand is access function (in real) return real; такая запись говорит о том, что Integrand это указатель на соответствующие функции, и когда описываем процедуру интегрирования, то одним из параметров может выступать Integrand.
procedure Integral (F:Integrand; ...) // описание заголовка процедуры
Integral (EXP ' access; ...); // вызов процедуры, т.е. как бы явным образом берем адрес от конкретной функции (в нашем случае экспоненты), слово access здесь присутствует в поддержании обей концепции читабельности (readability) языка Ада
Такие новшества в Аде 95 были вызваны изменениями требований к языку, в 1995 году Ада была не единственным, а одним из многих яп и для того чтобы эффективно работать на этом языке было необходимо связывать программы на этом языке с программами на других языках, которые в большинстве своем были написаны на Си. В большинстве своем появление подпрограммного типа связано именно с этим. В Си и Фортран необходимо передавать указатели на соответствующие функции и процедуры, такую концепцию можно использовать, хотя она и не совсем надежна, для динамической параметризации соответствующих модулей. Заметим что статическая параметризация, которая была в Аде 83 и осталась в Аде 95, она с точки зрения компиляции немного более эффективна.
2. Современные языки
А) Delphi не рассматриваем, так как это эклектичный язык, т.е. язык - наследник Паскаля, кроме того присутствуют вкрапления из Си, Модула 2, и еще объектно-ориентированная нашлепка.
Чисто объектно-ориентированный язык
Б) В языке Java указателей нет вообще. Как написать процедуру интегрирования? Нужно понимать, что понятие функции и процедуры пришло из императивных языков, Java поддерживает объектно-ориентированную парадигму. Программа на языке Java есть набор классов, кроме классов у нас есть еще экспорт и импорт пакетов. Всё интересное происходит внутри классов. class X {...};
Процедура интегрирования не имеет смысла – имеет смысл некий интегрирующий класс, можно написать, что Integrand это некоторый класс, который содержит внутри себя метод, который и будет интегрирующей функцией. и процедура Integral она что делает, класс Integrand рассматривается как класс и вместо аргумента вызывается метод – интегрирующая функция. И как нам проинтегрировать конкретную функцию – мы делаем свой экземпляр класса Integrand, унаследуем от него от метод. Например, функцию EXP переопределим как интегрирующий метод. Всё на свете должно быть сведено к классам и методам их доступа. И в принципе механизм наследования он в общем случае более общий чем механизм параметризации. Можно было с помощью параметров ввести понятие функции, однако если у нас есть динамическое связывание методов, то этого в принципе достаточно, чтобы промоделировать параметризацию произвольных объектов на основе наследования. Стиль программирования меняется, зато у нас полная надёжность - никаких фокусов с указателями в языке Java быть не может.
В) Интересная ситуация с языком 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. ЯП с классами - есть понятие класс, с одной стороны является обёрткой (сводит множество значений и множество операций), но ещё он является и типом (дуальность классов).