лекции (2004) (1160823), страница 11
Текст из файла (страница 11)
Рассмотрим вопрос параметризации процедур. У каждой процедуры мы указываем набор параметров.
Формальные параметры – переменные языка, но особого рода.
Фактические параметры - выражения или переменные (частные случаи выражения).
Фактические параметры <=> (связывание) формальные параметры.
Связывание всегда динамическое, т.к. процедура имеет смысл, как динамические абстракции.
Понятие абстракции:
-
определение
-
вызов- конкретизация соответствующей абстракции(происходит связывание фактических и формальных параметров).
Есть 3 семантики (эффекта, а не способа) связывания:
1. Входные параметры (параметры, от которых требуется только значение); мы используем только значение фактических параметров, они не меняются.
2. Выходные параметры –параметры не обязаны иметь начальное значение, но могут быть изменены.
3. Изменяемые параметры - требуется и исходное значение и возможность его изменения.
В Аде:
1-in
2-out
3-inout
procedure P (X: in T; Y: out Z; w: inout TT)
Входным параметром может быть произвольное выражение.
Для выходных - это только переменная, которая занимает фиксированное место в памяти.
Для изменяемых параметров - переменная, которая может не занимать фиксированное место в памяти, но она обязана иметь своё значение.
В большинстве ЯП вместо указания семантики передачи параметра указывается способ (механизм) передачи параметра.
Способы передачи параметров:
1. По значению
2. По результатам
3. По значению/результатам
4. По ссылке/адресу
5. По имени.
1-й, 2-й, 3-й способ:
Любой формальный параметр (ФП) есть некоторая локальная переменная. Место для локальных переменных отводится в стеке. Там же отводится место и для ФП.
1-й способ - значение фактического параметра копируется перед call в ФП. Все изменения ФП связаны с изменением локальной переменной не сказываются на фактическом пар-ре. Фактический параметр- любое выражение, которое вычислится при копировании в переменную. Главное- совпадение типов.
2-й способ - перед возвратом значение ФП копируется в фактический параметр.
3-й способ - перед CALL копируем фактический параметр, а перед return копируем всё обратно.
Эти способы передачи параметров один в один соответствуют 3 семантикам связывания.
Во всех современных ЯП используется только 1-й вариант из этих 3-х.
2 и 3 включают в себя копирование при больших размерах параметра- накладно. 1 используется в основном для простых типов данных.
Ещё в современных ЯП используется 4-й тип - по ссылке.
4-й способ-Передача по ссылке - фактически, это передача адреса по значению(в Си вызывается операция взятия адреса) изменение фактического параметра этот способ может реализовать все 3 семантики: in, out, inout.
Если мы хотим передавать параметр по ссылке (т.к. он велик, и мы не хотим его копировать), но не хотим, чтобы его меняли, то для этого, например, в Си++ введён механизм константных ссылок.(Нет в Паскале, Модула-2, Оберон)( Си++ отличается от Си введением ссылочного тд):
const T& a;// T- как правило, структурный тд
const int& a;// обычно не используется
способ- по ссылке, семантика- входная cсоответствующую ссылку мы не можем менять можем передавать не только адрес, но и ссылку на константные объекты(формально адреса в Си++ не имеет).
LHS - то, что может находиться в левой стороне оператора присваивания.
1). В Аде-83 компилятор выбирает сам способ передачи параметров, программист указывает только семантику.
2). С- только по значению.
3). С++-1,4 способы.(входной параметр либо по значению, либо константной ссылкой, выходной параметр - по ссылке)
4). Паскаль, Модула-2, Оберон- 1,4 способы(семантику указать не можем)
5). Java - есть простые типы данных и референциалные.
Все простые типы данных - по значению, а референциальные (ссылки) - по ссылке
(в общем все по значению, в том числе и ссылка)
void f (int a){}// по значению
class X {...}
void f (X a)// ссылка
Передача по ссылке объекта простого типа.
Любому тд соответствует встроенный в базис языка тип, например:
int -> Integer
Java.lang - специальный пакет, содержащий все объявления классов стандартной библиотеки, интегрированных с языком. По умолчанию имена из этого пакета импортируются в файл. (string, Object, все классы-обертки и т.д.) Если класс-обертка появляется в качестве ФМ функции, то соответствующим фактическим параметром может быть выражение соответствующего типа:
void f (Integer a)
то, используя упаковку-распаковку:
int x;
f(x) ~ f (new Integer (x))
new Integer (x)- новый объект передача по ссылке любые изменения со ссылкой отражаются на х и при выходе происходит выделение объекта х.
В C# аналогично с Java, но мы должны перед именем параметра указать ref:
void f(ref int a);// а по ссылке Int32 + boxing и unboxing
f(ref x)- для структур ref необходим
класс Х:
void f(ref X a)- ссылку передаем по ссылке(можем менять как ссылку, так и объект), ref относится к ссылочным тд
В языке Java мы должны создать класс
Ref X {X a;}- здесь мы ссылку менять уже можем и передаем по ссылке., сами моделируем.
В языке С# выходной параметр по ссылке(отражение семантики):
ref out int a; от а не требуется входное значение.
void f(ref int a); - 3-й тип параметров (изменяемые параметры)
От объекта а требуется статус определенности, его можно изменять!
X a = null; - нормально проинициализировали значение.
X a; - несуществующее значение - при попытке передачи параметра по ссылке будет ошибка.
Т.о. входной параметр – по значению, выходной параметр по ссылке с модификатором out и ref необходимо указывать и при вызове соответствующей функции. Изменяемый параметр- передаем по ссылке требуется начальное значение.
6). В языке Дельфи есть передача параметров по значению и по ссылке( комбинация Паскаля и объектно-ссылочного языка). Значениями объектов являются ссылки, поэтому все комбинации соответствующих средств у нас обеспечены.
Во всех современных ЯП специально специфицируется способ передачи параметров и способ указать семантику средств.
7). В Ада 83 специфицируется только семантика, а компилятор сам определяет способ передачи.
Передача простого значения (например 1 адресуемая ячейка, которая часто используется) по ссылке (2 разыменования указателя ) менее эффективна, чем по значению(2 копирования в конце и начале)
Ада 95:
In простые типы- по значению, непростые - по ссылке.
Inout- все параметры по ссылке
Out- все параметры по ссылке
Из-за требований переносимости:
P(X: out integer; Y: out integer)
X := l;
... // происходит авария
Y := l;
A, B:INTEGER;
P(A, B);
Если процедура работает нормально, то всё ОК.
А если в середине процедуры происходит авария и процедура P не завершается нормальным образом и параметры передаются по ссылке, то параметр А поменяет свои значения. Если параметры передаются по значению, то А не изменится, т.к. присваивание происходит при нормальном выходе из процедуры, если выход не нормальный то мы не знаем присвоилось что-нибудь х и y.
Если для процедуры побочный эффект - норма, то стоит ли создавать функции с побочным эффектом?
Побочный эффект для функции вещь очень неприятная(70-80гг 20в):
В языке должны быть:
- procedure( с побочным эффектом)
- function F(in ...) (без побочного эффекта) входной класс in и только входные параметры
- procedure F (in out inout) return T; - процедура, возвращающая значение.
Побочный эффект - функция модифицирует свои параметры и функция, которая модифицирует глобальные переменные (что практически невозможно проверить- ошибка) Аде-83 остались только процедуры и функции с параметрами in.
(рор()-процедура для Ада, т.к. она меняет значение стека)
5-й способ передачи параметров - передача по имени. Это и есть полное отождествление формального и фактического параметра(похоже на макроподстановку).
Если адрес фактического параметра не меняется, то различий по способу передачи по имени и по ссылке нет., в противном случае разница существенна.
int x[20]; - некоторый массив
int i;
P(int a, int i)
P(x[i], i)
по ссылке- зависит от i
i = 2;
a = 0; // x[2] = 0;
i = 3;
a = 1; // x[3] = 1;
по имени- действительно x[i]
Algol-60
Впервые применен способ передачи параметров по имени.
Параметры можно было передавать по значению.
Если параметр менял свое значение, его можно было передавать по имени.
Реализация
thunk - маленькие подпрограммки. Для каждого параметра, переданного по имени, связывалась такая подпрограммка, вычисляющая адрес этого параметра.
Обращение к этому thunk'у было каждый раз, когда мы обращались к параметру внутри функции!
И именно это стало причиной редкой неэффективности Алгола 60.
Во многих функциональных языках программирования передача параметров по имени является наиболее естественной и адекватной.
Процедурные типы данных или передача функций как параметров
В языке Модула-2 был введён специальный процедурный тип:
TYPE P = procedure (INTEGER; VAR REAL):REAL
Procedure-указатель, что это процедура
(INTEGER; VAR REAL)- параметры( var- переменная)
REAL- возвращаемое значение
Значением Р- все функции, которые удовлетворяют этому прототипу.
PROCEDURE F(X:INTEGER; VAR Y:REAL):REAL;
a:P;
a := F;- правильно!
Можно написать:
D,Z:REAL;
D := a(D,Z);
а ведет себя, как F
В Обероне и turbo Рascal такая же семантика.
В C и C++ явно говорится об указателе на функцию.
int f(void) – прототип функция
int (*f)(void) - указатель на функцию
typedef int (*f)(void) – f- синоним соответствующего типа
В реализации функционального типа процедура - указатель. Вещь это крайне не надёжная. Следовательно, операции с ними надо сильно ограничивать, для языков, которые претендуют на надежность:
-
Ada
Ada-83 функциональных типов нет, иначе- введение указателя, которого разработчики очень опасались. Передача процедур решается с помощью родовых модулей)
Родовые сегменты (функции, процедуры, модуля) - аналог шаблонов в Си++. Инициализация происходит статически на этапе компиляции.
Ada-95 вернулся процедурный тип данных и указатели для обычных переменных.
Языки программирования.
Лекция 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 быть не может.