На прошлой лекции мы начали рассматривать тип данных указатели/ссылки

с. 1 с. 2
На прошлой лекции мы начали рассматривать тип данных указатели/ссылки. На этой лекции мы продолжим рассмотрение этого типа данных и рассмотрим функциональный тип данных.

Две основные проблемы, которые связаны с типом данных указатели, связаны с тем, что обычно указатель используется для ссылок на динамические структуры данных, которые расположены в динамической памяти. По крайней мере, в строгих языках программирования, к которым с этой точки зрения относятся стандарт Паскаль, Ада 83, Оберон, с одной стороны, есть понятие указателя, а с другой стороны это понятие указателя, особенно в Аде 83, ограничено таким образом, что указатели ссылаются только на объекты из динамической памяти. Для таких указателей мы отметили две главные проблемы:



  1. Мусор

  2. Висячие ссылки.

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


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

Кроме этого здесь есть еще один аспект. Многие версии Паскаля, прежде всего Турбо Паскаль, Delphi, как наследник Турбо Паскаля и, конечно же, такие языки как С и С++ - это языки нестрогие в том плане, что там понятие указатель расширено, а именно: указатель – это не только ссылка на объект динамической памяти, в этих языках есть понятие адресной операции, которая выглядит в С - &, в Delphi - @. Т. е. можно взять адрес произвольного объекта и присвоить его указателю. В результате никакой видимой разницы между двумя типами указателей нет. И в таких языках, наряду с этими двумя проблемами появляются проблемы, связанные с тем, что мы можем пользоваться указателем, например на автоматический, то есть квазистатический объект, размещаемый в стеке, мы уже покинули соответствующий блок, а указатель продолжает существовать. Эта проблема очень похожа на проблему висячей ссылки, т. е. указатель у нас определен, куда-то указывает, но про содержимое этой области памяти мы ничего сказать уже не можем потому, что она освобождена. Т. е. это проблема, связанная с висячей ссылкой, но которая получается за счет того, что мы можем брать любой объект памяти, в том числе и статический. Кроме этого появляется проблема ошибочного освобождения памяти, т. е. мы освобождаем указатель, который на самом деле указывает не на динамический объект.

Эти ошибки очень опасны, потому что они проявляются совершенно не там, где их ждешь, и поэтому их устранение очень накладно. И поэтому во многих языках, прежде всего в этих строгих, отказались от идеи того, что указатель указывает на произвольный объект. Например в Аде 83, как уже говорилось, единственная возможность проинициализировать указатель – с помощью операции new T, ну и, разумеется, операции присваивания :=. Если реализация Ады позволяла динамическую сборку мусора, то тогда соответствующее решение было весьма и весьма надежным. Но с одной стороны, как уже говорилось, реализация Ады допускает и нединамическую сборку мусора, а с другой стороны ситуация в 90-ых годах несколько изменилась. И интересно, что в Аде 95 создатели языка были вынуждены расширить язык таким образом, чтобы допустить указатели и на нединамические объекты. Спрашивается: зачем, ведь, как мы выяснили, это еще более добавляет ненадежности в указатель, а указатель – это самый ненадежный простой базисный тип данных в ЯП. Что изменилось с 1983 года? Подходы к надежности – нет, Ада 95 с точки зрения надежности исповедывала те же критерии, что и предыдущий стандарт языка – Ада 83. Изменились внешние условия: если создатели Ада 83 были уверены, что они проектируют единый универсальный ЯП, который должен заменить все существовавшие к тому времени в соответствующей проблемной области языки программирования. Ситуация в 95-м году понятно была какой – было ясно, что новый стандарт Ада не может претендовать на роль всеобъемлющего ЯП. В результате связь программ на языке Ада с программами на других языках из чисто опционального (какого-то дополнительного) средства, как это было в момент создания первого стандарта Ада, превратился в насущную необходимость. Для чего нужны указатели на нединамические объекты? Прежде всего, чтобы можно было вызывать программы на других ЯП и соблюдать соглашения о вызовах, соглашения о типах параметров, которые приняты для тех ЯП. Чаще всего вызываются программы, написанные на С, на Фортране. Для вычислительных программ – подпрограммы на Фортране, для системных программ - это интерфейсы на С или С++, а в этих языках все системные интерфейсы разработаны так, что без указателей там не обойтись. Cпрашивается: можно ли попытаться сохранить достаточно надежным язык? Создатели Ада 95, за счет, естественно, некоторого усложнения языка наши достаточно приемлемое решение для сохранения надежности. Новые проблемы возникли с тем, что, скажем, в том же самом С++, внешне никак нельзя отличить указатель, размещенный с помощью процедуры new от указателя, который мы получили с помощью адресной операции. Вот от этого свойства указателей и надо отказаться. В языке Ада есть обычный указательный тип а-ля Ада 83

Type PT is access T;

Конструкция очень синтаксически похожа на обычное описание указателей на других ЯП, основанных на Паскале. Ну и если у нас есть некоторая переменная

X1:PT;


то в этом случае мы можем размещать X1 с помощью оператора new, который вызывает менеджер динамической памяти.

X1:=new T;

Эти указатели в языке Ада 95 остались. Но появился еще новый указательный тип, а именно

Type PTT is access all T;

Это означает, что указатели PTT могут ссылаться как на динамические объекты, так и на нединамические. Если мы опишем переменную

X2:PTT;


то мы можем, во-первых, использовать и старый способ размещения динамической памяти и, как следствие, способ присваивания указателя

X2:=new T;

Но кроме этого можно еще адресовать и объекты из нединамической памяти, но не любые объекты, а только те, которые специально для этого предназначены. Пусть у нас есть какая-то переменная

Y1:T;


и переменная Y2, которая описана с помощью ключевого слова aliased

Y2:aliased T;

И то и то относится к одному и томуже типу данных, однако ключевое слово aliased говорит о том, что дополнительно к числу операций, которые можно проводить над типом T добавляется еще операция вычисления адреса, а именно допустима конструкция

X2:=Y2'access;

Мы получили в Х2 адрес переменной Y2. При этом соответствующий указатель должен быть объявлен с атрибутом all.

X1:=Y2'access;

ошибка потому, что слева соответствующий указатель не может воспринимать адресную операцию.

X2:=Y1'access;

ошибка, связанная с тем, что указатель Х2 относится к типу PTT, но, однако, к Y1 не применима операция access. В результате проблемы, о которых говорилось ранее, здесь снимаются. И в результате можно передавать и адреса нединамических объектов, скажем функций, написанных на языке С. Это скорее всего будут системные вызовы из соответствующей библиотеки, или из интерфейса прикладного программиста API. Поэтому эта проблема, разумеется, снялась. И это, конечно, достаточно неплохое решение.

Еще в языках основанных на С, на С++ есть понятие адресной арифметики, которая, во общем-то приводит к тому, что надежность соответствующих языков вызывает очень и очень большие сомнения.

В более строгих языках, чем С и С++ операции над указателями фактически ограничены следующим образом: во-первых операция присваивания := , кроме этого операция размещения в динамической памяти new, ну и еще в языке Ада для некоторого вида указателей адресная операция access, но адресная операция, которая защищена с помощью соответствующих конструкций. И если в языке реализована динамическая сборка мусора, то есть у нас отсутствует оператор типа delete, то решение с указателем получается вполне надежным. То есть никаких проблем с указателем, о которых мы говорили, у нас нет. И поэтому если мы определим указатели таким образом, что крайне ограничим им набор операций: только присваивание и только выделение динамической памяти и больше никаких других операций (ну и естественно передача как параметр, что эквивалентно присваиванию) то получившаяся конструкция указателя становится вполне надежной. Но только ее уже не надо называть указателем, потому что слово указатель в определенного рода кругах вызывает недоверие. И, следовательно, тогда такой ограниченный тип данных с точки зрения множества значений все равно остается адресом, и имеет смысл переименовать его в такой тип данных, как ссылка. И действительно, в современных языках программирования происходит интересный процесс, а именно: у нас появляется новый ссылочный тип данных. Это языки Delphi, Java и C#. В Java, например, все типы данных делятся на:

а) простые типы данных, которые мы уже рассматривали. Это, прежде всего, арифметические, логические, символьные и т. д..

б) ссылочные типы данных, к которым относятся массивы, классы и интерфейсы. И все эти данные описываются с помощью ссылок.

Та же самая ситуация и в языке Delphi. Там есть понятие класса – новое понятие. Так вот объекты классов в языке Delphi это исключительно ссылки и то же самое относится и к С#. Но по сравнению с Java, Delphi и C# несколько перенавороченны, а именно: в Delphi есть еще старое понятие записи, унаследованное из Турбо Паскаля, а в C# есть еще понятие структуры, которое похоже на урезанный класс. Когда мы будем говорить о составных типах данных, мы поговорим о структурах языка C#. Но в целом, именно основные типы данных, выражаемых классом, в этих языках выражаются с помощью ссылок.

Рассмотрим размещение переменной. Пусть у нас есть класс Х. Мы объявляем объект х класса Х

Х х;


В принципе это объявление синтаксически верно и на языке Java, и на языке C++, и на языке C#. Но на языке С++ это значит совершенно другое, а именно то, что в С++ объект соответствующего типа данных размещается в памяти, в какой - это зависит от того где находится это объявление: либо в квазистатической памяти, если оно находится, например, внутри блока, либо в статической памяти, если оно объявлено она верхнем уровне. Если же речь идет об объявлении в С# или Java, то это на самом деле размещается только ссылка на объект класса Х. То есть, везде, где речь идет об именах объектов, нужно понимать, что это ссылки, причем то же самое относится и к массивам. Например, в Java

char a[];

Причем в Java, как и в C# запрещено даже указывать здесь длину массива. Здесь речь идет не о том, что размещается какой-то массив в памяти, а размещается просто ссылка на соответствующий массив. При этом в начальный момент времени они никуда не указывают.

х


а

Соответствующие объекты должны быть обязательно проинициализированы с помощью оператора new.

х= new X;

a= new char[20];

Именно в момент выполнения операции new происходит инициализация ссылки на соответствующий объект






х Х



а


В Delphi синтаксис немножко другой – там речь идет о вызове конструктора. То есть если мы описываем объект Х типа Т

X:T;

то опять же размещается только ссылка на объект типа Т, в начале неинициализированная, а инициализировать ее мы с помощью явного вызова конструктора



X:= T.Create;

Это языки с так называемой ссылочной семантикой. И как следствие необходимо всегда помнить, что в этих языках, например, если у нас есть Х1, Х2 типа Т

X1,X2:T;

или соответственно в таких языках как C# или Java

Т Х1,Х2;

То присваивание

Х1=Х2

означает, поскольку здесь речь идет только об именах ссылок, только присваивание ссылок. Никакого реального копирования объекта не происходит. То же самое распространяется и на массивы в этих языках, поскольку массив также имеет ссылочный характер, и все размещается исключительно в динамической памяти. Поподробней о проблеме копирования мы будем говорить, когда будем обсуждать концепцию класса. А сейчас отметим, что у нас появилось понятие ссылки, которое, с одной стороны обладает мощностью указателя, с точки зрения обращения к динамической памяти, с другой стороны оно абсолютно безопасно по сравнению с общим понятием указателя.



То, что называется ссылкой в языке С++ - это несколько особый тип данных, который тоже служит для того, чтобы играть роль указателя, но в то же время ограниченного с точки зрения набора операций. Иначе говоря, ссылки в языке С++ это некоторый аналог имени объекта. Единственная операция, которую можно производить со ссылкой – это явная операция – обязательная инициализация. Т. е. если в языке С++ у нас есть переменная i

int i;


Есть ссылочный объект х.

int &x;


- такое объявление у нас не допустимо. Мы должны обязательно, если речь идет о переменной типа ссылка, проинициализировать

int &x= i;

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

x=3;


эквивалентны тому, что вместо х употребляется i. Разумеется подобного рода стиль программирования мягко говоря нехорош, хотя допустим в С++.

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

int f(int);

int f(int&);

Понятно, что в первом случае компилятор при вызове

f(i);


передаст значение переменной i, а во втором случае он передаст адрес. Как следствие, раз в языке есть несколько способов передачи параметров, то есть требование обязательного прототипирования всех функций, т. е. все функции должны быть обязательно описаны перед их употреблением, иначе компилятор просто не будет знать по какому способу передавать параметры, синтаксически-то вызов будет одинаковым. Ну и дополнительно можно использовать такое полезное свойство ссылок, когда нам действительно нужен доступ к объекту. Скажем если у нас есть какой-то контейнер (массив, или какой-то более общий), то функция, которая возвращает ссылку, просто дает возможность прямого доступа к этому элементу.

Не случайно ссылка – это не просто некоторый каприз Строуструпа, хотя, конечно понятие ссылки очень удобно именно для единообразного способа передачи праметров. Можно было бы, если речь шла чисто о передаче параметров, ввести, как в Паскале просто при описании синтаксиса переменных какое-то дополнительное ключевое слово, например, нечто типа ref

int f(ref int);

которое просто говорит о том, что соответствующий параметр передается по ссылке, и не нужно раздувать базис языка, не нужно добавлять новый тип данных. Спрашивается, почему Строуструп пошел по другому пути? Он, все таки, добавил новый тип данных в базис языка. Обратите внимание, что за исключением типа данных bool, который появился только в 90-ые годы, ссылочный тип данных - это единственное расширение базиса языка. С точки зрения базиса и С, и С++ совпадают с одним единственным исключением, а именно с ссылочным типом данных. Где еще нужен ссылочный тип данных? Вспомним, что уже на ранних стадиях развития языка С++ Стоуструп проводил идею перекрытия операций. Идея, как видите, плодотворная, поскольку во всех современных языках программирования идея перекрытия имен процедур и функций реализована. Она оказалась очень удобной. Единственное исключение – это язык Оберон, поскольку в принципе вещь удобная, но теоретически без нее обойтись можно. Перекрытие операций – вещь полезная, но кроме этого Строуструп реализовал перекрытие и стандартных знаков операций. При этом из всех языков, которые позволяют такое перекрытие, С++ обладает наиболее богатым объемом операций. Т. е., как уже говорилось, нельзя перекрывать только операции доступа к элементам . , операцию разрешения области видимости :: , связанную с этим операция доступа к указателю на член класса .*, ну и нельзя еще перекрывать трехместную операцию ?: ,причем, как написал Строуструп: "только по причинам, что я не придумал ни одного нормального контекста, в котором это перекрытие могло бы оказаться полезным", а так, почему бы и эту не перекрыть. Все остальные операции, включая и операцию вызова функции ( ) , и операцию разименования указателя -> и многие другие операции можно перекрывать, в том числе и операцию индексирования [ ]. Спрашивается, а как можно перекрыть операцию индексирования, чтобы она была функционально эквивалентна обычной операции индексирования в том же самом языке, как С.Если у нас есть массив

int a[10];

у нас есть операция индексирования объекта, например

a[0];

Cпрашивается, какой тип данных возвращает а[0]?



Если a[0] возвращает просто int, то мы можем писать

int i = a[0];

но мы можем писать и

a[0] = i;

Здесь это уже не просто значение целого типа данных, мы не можем суда подставить значение целого типа данных – в левой части оператора присваивания должно содержатся нечто, что имеет адрес. Указатель, разименнованный указатель – ради бога, поскольку *р слева мы можем писать потому, что она имеет адрес, константу не можем писать, поскольку константа адреса не имеет. То есть a[0] должно обозначать нечто, что дает адрес. Но с другой стороны, адрес у нас есть – это указатель, но указатель нужно еще разименовывать. И без введения типа данных ссылка нельзя было перекрыть ряд операций, поскольку операция индексирования возвращает именно ссылку на объект, и используя эту ссылку мы можем, например, модифицировать содержание этого объекта, брать из него значение, все, что угодно. Все, что можно делать с объектом, мы можем делать и с ссылкой. И все так называемые операции над ссылкой, за исключением операции инициализации это и есть операции с объектами на которые ссылается данная ссылка. И следовательно, введение ссылочного типа данных – это не просто каприз, а расширение базисной системы типов на всего один только тип ссылка позволило действительно сделать язык достаточно мощным. Без ссылок перекрытие ряда стандартных операций просто никакого бы смысла не имело. С этой точки зрения, конечно, ссылки в языки С++ несколько особый класс, который немножко отличен от понятия ссылки в таких языках, как С#, Delphi, Java.

У нас остался еще один тип данных, а именно – функциональный тип данных. Значениями функционального типа данных служат процедуры и функции. Такой тип данных есть например в расширении Паскаль, Турбо Паскаль, например, и как следствие в языке Delphi, Функциональный тип данных появился в языке Модула 2, в частности для того, чтобы можно было передавать процедуры и функции как параметр потому, что, вспомним – в стандартном Паскале у нас в дополнение к двум обычным способам передачи параметров, а именно – по значению и по ссылке с помощью ключевого слова var, добавились еще 2 специальных способа параметра, а именно – параметр-процедура и параметр-функция. Вот в Турбо Паскале уже параметров процедур и функций просто нет потому, что там появился функциональный тип данных. То же самое произошло и с языком Модула 2 и, как следствие, с Обероном. Там появились соответствующие функциональные типы данных. Например, если хотеть написать некоторый тип

type Prc = procedure(var integer);

Prc – процедура, у которой есть один формальный параметр. Это синтаксис Модула 2, в Турбо Паскале и в Delphi здесь необходимо указывать переменную, но в общем смысл один и тот же. И вот теперь у нас есть тип данных, мы можем описывать как переменные, так и формальные параметры типа Рrc. Что может являться значениями такого функционального типа данных? Имена других процедур и функций, которые описаны в программе. Каким образом реализуется такой функциональный тип данных? Заметьте, что и в С и в С++ можно описать это логически. Это будет нечто такого вида

typedef void (*f(int &));

f является теперь именем соответствующего функционального типа данных. С точки зрения реализации функциональные типы данных – это адреса соответствующих процедур потому , что теперь мы вполне можем описать

VAR X: Prc;

Присвоить Х какую-то процедуру Proc

X:= Proc;

где Proc – это процедура, которая имеет такой прототип и описана у нас в программе. Ну и понятно, что с точки зрения реализации теперь это просто адрес процедуры. Мы теперь можем писать



Х(i);

и вызывать соответствующую процедуру. Получается так, что вместе с функциональным типом данных в наш язык пролезает понятие указателя, и именно из этих соображений функциональные типы данных тоже можно считать ненадежным, прежде всего потому, что указатель можно забыть проинициализировать и прочее, и прочее и прочее. И поэтому, конечно, в языке, который особое внимание уделяет надежности, понятие функционального типа данных должно быть урезано. Отметим, что в традиционных языках программирования основное назначение процедурного типа данных – передача процедур и функций как формальных параметров, ну и кроме этого еще это обработчики всякого рода событий и всего того, что в английском языке именуется словом callback. Callback – это какая-то процедура или функция, которая вызывается в ответ на какие-то внешние воздействия, например передачу сообщений . Они обычно реализуются с помощью функционального типа данных,



т. е. с помощью указателя на процедуру или функцию, со всей, связанной с этим ненадежностью и т.д. и т.п..

Как с точки зрения надежности пытаются решить проблему функциональных типов данных дизайнеры языков программирования? Возьмем язык Ада. Язык Ада, вообще говоря, всегда очень аккуратно поступает с указателями. Спрашивается: что он будет делать с указателем на функцию? Тут ситуация следующая: в языке Ада 83 все было очень просто, а именно функционального типа данных там вообще не было. А как же тогда написать процедуру вычисления интеграла? Очевидно, в этой процедуре в качестве одного из параметров должна передаваться подынтегральная функция, а писать для каждого интеграла особую процедуру, которая будет вызывать эту функцию, вообще говоря, достаточно не удобно. Но создатели языка Ада решили эту проблему следующим образом. Как уже было сказано, основное назначение процедурных типов данных – это либо передача параметров процедур и функций, либо моделирование понятия callback. И то и другое оказалось возможным именно за счет ограниченности функционального типа данных. Значения его ограничены заранее известным, то есть статическим набором процедур и функций. Те процедуры и функции, которые описаны в вашей программе – это и может быть допустимое значение функционального типа (с нужным прототипом). Следовательно, все возможные значения статически известны, и следовательно, связывание формальных и фактических параметров можно провести не на стадии выполнения, передавая адреса соответствующих процедур и функций, а на стадии трансляции. В Аде есть так называемое понятие родовых сегментов (generic segment). Они несколько похожи на шаблоны языка С++. Как шаблоны языка С++, так и родовые сегменты Ада – это есть статически параметризованные конструкции. В отличие от процедур и функций, которые параметризовывать можно только объектами данных, родовые сегменты можно параметризовать именами типов, именами процедур и функций и т.д.. Когда мы будем разбирать статическую параметризацию – это отдельная глава в языках Ада и С++ (в других языках, которые мы рассматриваем статической параметризации просто нет) мы увидим, каким образом можно написать, например, ту же самую процедуру интеграл, но сделать ее не динамически настраиваемой, а статически настраиваемой. И в принципе, с точки зрения гибкости использования это никак на эффективность использования языка не влияет. То же самое можно сделать и с концепцией callback, т. е. некоторой процедурой - обработчиком внешних событий. В языке Ада 83 есть понятие задачи, а именно параллельного процесса. Так вот задачный тип данных как раз не опасен потому, что он служит абстракцией понятия процесса, а процесс это все-таки не указатель, это другая сущность. А подпрограммный тип данных был не нужен. Интересно то, что в Аде 95, в которой было много расширений языка, в частности мы обсуждали расширение понятия указателей, подпрограммный тип данных появился. Спрашивается: что же изменилось? Как и с концепцией указателя язык изменился за счет того, что изменились требования к языку, изменилась программная среда вокруг. В Аде 95 подпрограммные типы данных понадобились из тех же соображений, почему понадобились указатели на нединамические объекты – потому, что необходимо было обеспечивать интерфейсы с другими ЯП. И если интерфейс с языком С, то там без понятия указателя не обойтись, и как следствие появилась новая концепция – расширение концепции указателей. Но кроме этого, обойтись без передачи функций как параметров в языке С не возможно и, следовательно появился подпрограммный тип данных. При этом, если бы речь шла о программировании на одном и только одном языке, можно было бы обойтись без этого типа данных. А в случае, когда нужно обеспечивать интерфейсы с другими ЯП, подпрограммный тип данных оказался просто необходим. Но тем не менее, проблема с ненадежность функционального типа данных все равно остается. Решают ее в разных языках по-разному. В языке Delphi эта проблема никак не решена потому, что Delphi обеспечивает совместимость с Турбо Паскаль, и там все старые средства Турбо Паскаль, в том числе функциональный тип данных, остались. Язык Java также решил эту проблему очень просто – функциональный тип данных в языке Java отсутствует. Спрашивается: а как же тогда можно передавать процедуры и функции в качестве параметров? А не нужно передавать процедуры и функции как параметры. Java отличается от языка Ада 83 тем, что там есть очень мощное понятие класса. Класс интегрирует в себе и данные и операции. Поэтому, например, одно из решений этой проблемы это, вообще говоря, если нам нужна функция, мы завертываем ее в особый класс. Те, кто занимался изучением стандартной библиотеки шаблонов в языке С++ STL, вспомнят специальные, так называемые функции-классы, которые есть в стандартной библиотеки шаблонов. Это некоторый класс, в котором переопределяется операция вызова функции ( ). Основным назначением такого класса – держать эту функцию. Из этогокласса не с помощью механизма вывода, а с помощью статической параметризации, для эффективности, мы переопределяем соответствующим образом, настраиваем эту операцию вызова, а все алгоритмы, которые работают с этим классом, знают, что он реализует функцию, которую можно достать по имени объекта класса, применяя к нему эту перекрытую операцию ( ). То же самое предлагается сделать на языке Java, а именно: мы определяем некоторый класс. В языке Java, правда, статической параметризации нет, но зато там есть динамическое связывание методов. И вот мы определяем некоторый класс. У него, соответственно, определяем некоторую функцию f. И в результате, если нам нужно, скажем, написать процедуру интеграл, то соответственно мы говорим, что вместо функции мы вызываем некоторый класс с, и в качестве подынтегральной функции у него будет f. И в результате, если нам надо написать процедуру интегрирования, для какой-то подынтегральной функции ff. Мы выводим новый экземпляр класса с и в нем переопределяем функцию f так, чтобы она вызывала или делала тоже самое, что и функция ff. В результате проблема с передачей функций как параметров решена с помощью ведения такого понятия как класс-функция. Это не языковое понятие, это понятие методов программирования. Когда мы будем рассматривать подробнее концепцию производных класса, в частности в языке Java, мы увидим, что в Java появились даже специальные конструкции - анонимные классы, которые как раз упрощают программирование такого рода классов, чтобы не придумывать дополнительные имена.

Что же сделано в C# с этой точки зрения? Традиционный процедурный тип данных там реализовывать не стали, из-за проблем, связанных с ненадежностью понятия процедурного типа данных. Там естественно можно использовать метод языка Java, но разработчики C# пошли несколько по другому пути – они ввели понятие делегата. Сейчас мы не будем обсуждать понятие делегата в языке C# - мы вернемся к нему позднее, когда будем рассматривать концепцию класса и наследования. Сейчас просто отметим, что понятие делегата, с одной стороны, выглядит как тип данных, а с другой стороны, его значениями могут являться только функции. При этом объекты этого типа данных мы также должны с помощью соответствующей операции new, указывая конкретную функцию, которая будет служить в качестве значения этого делегата. При этом приятная особенность делегата, например, по сравнению с данными обычных функциональных типов – то, что, на самом деле, делегат – это не один указатель на какую-то функцию, а его можно рассматривать как совокупность указателей. Т. е. у нас есть понятие callback – какого-то обработчика внешних событий и, если при традиционном стиле программирования у нас это единственный указатель на одну функцию, который вызывается в случае возникновения какого-то внешнего события, то с делегатом даже проще: делегат – это совокупность обработчиков. Т.е. можно на одно и тоже событие повесить несколько обработчиков – так называемое broadcast , т.е. широковещательная обработка события и, в результате вызова делегата по очереди вызываются все функции, которые были добавлены в соответствующий делегат. В частности, например в языке C# есть отдельный тип данных событие, который представляет из себя частный случай делегата. И эти события, вообще говоря, интегрированы в систему языка. Когда мы будем говорить об объектно-ориентированном программировании, мы вернемся к понятию делегатов и к понятию событий в языке C#. На этом давайте закончим обсуждение простых типов данных.


с. 1 с. 2