Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > Object Pascal: кроссплатформенные технологии > Процедурный тип данных


Автор: SPrograMMer 16.4.2005, 20:31
Вот есчо один "наболевший" вопрос.

Процедурный тип данных. Что это такое и с чем его едят.

Подпрограмы в языке паскаль бывют двух типов: процедуры - подпрограммы (чаще всего) не возвращающие никакого результата, и функции, возвращающие какое-то значение.
Давайте представим, что нам нужно произвести какие-то действия с каким-то объектом, например с числом. Например, нам надо вычислить корень кубический этого числа, или возвести в 5-ую степень, или посчитать сумму чисел от 1 до этого числа или ....... Как можно реализовать всё это дело? Можно написать ряд подпрограммок , например с именами SQRT3, SQR5, Symma ... а в решающий момент (когда надо результат получить) мы просто будем вызывать нужную нам из перечисленных функций. Вот наверное первое, что приходит в голову для решения вышеуказанное задачи.
Хорошо мы с вами придумали 3 дествия с числом: корень, возведение в степень и сумму. Функций не много, реализовать не сложно, примерно так:
Код

function SQRT3(x:Real):Real;
Begin
....
End;
function SQR5(X:Real):Real;
Begin
.....
End;
function Symma(X:Integer):Integer;
Begin
....
End;
....
Begin
  ....
  Case чего-то of
      1: Result:=SQRT3(X);
      2: Result:=SQR5(X);
      3: Result:=Symma(X);
  End;
  // пользуемся значением Result в нужных целях далее
  ....
End;

Как я уже сказал функций не много, реализовать не сложно. Теперь смотрите, если мы такую программку составим, сдадим в пользование и через некоторое время нам скажут: "А вот если бы с числом можно было бы производить еще ряд операций" и выдадут нам список из еще 50 наименований функций. smile
Что будем делать, если текст общей программы большой, найти нужный кусок оччччччень затруднительно.
А вот здесь к нам напомощь и пришёл бы (просто прискакал бы) процедурный тип данных.
идея следующая: функции нам, хочешь - нехочешь, пришлось бы реализовывать в виде отдельных подпрограмм функций. а вот на вызове нужной мы с экономим: станим вызывать не саму функцию, а ссылку на неё.
Это очень напоминает работу с указателями и динамическими переменными, где по ссылке мы можем получить само значение. На самом деле так оно и получается, в машинных кодах и подпрограммы и пееменные имеют свои адреса, к ним (подпрограммам) и происходит обращение по адресу.
Для такой реализации в паскале надо обявить прототип функции, (с появлением .Net их стали называть делегатами) т е её объявление в общем виде: тип подпрограммы (процедура или функция), список параметров, с указанием типов данных и тип возвращаемого значения, если это функция.
Пример:
Код

Type
  TFunct=Function(X:Real):Real;
  TMyProc=Procedure(A,B,C:Integer;Var S:String);

Здесь обявлены два типа: TFunct - это функция, в качестве параметра которой выступает переменная X вещественного типа, результат - тоже вещественное число; TMyProc - подпрограмма, имеющая три параметра с именами A, B и C целого типа и параметра S строкового типа, который возвращает эта подпрограма. Как видите имена подпрограмм в общем виде не задаются.
TFunct и TMyProc теперь равноправные типы данных паскаля, такие как Integer, Real, Char, MyRecord, MyBigArray .... И соответственно можно объявлять переменные этих типов:
Код

Var
   F:TFunct;
   P:TMyProc;

Добавлено @ 20:32
На самом деле переменные F и P - указатели. Но к этим указателям нельзя применять адресную арифметику, к ним применяются только операции присваения и сравнения. В них может содержаться "пустой" укзатель, т е Nil.
Так как переменная F имеет тип данных TFunct, который обявлен как Function(X:Real):Real, то в эту переменную может быть занесена только функция, не подпрограмма!, у каоторой только один параметр, типа Real. Но и это не все функция должна возвращать значение тоже типа Real, если у функции один пааметр типа Real, но она возвращает значение типа Char или String или Integer или ...., то такую функцию в переменную F занести нельзя!

Что нам это дает? Неужели нельзя было обойтись простым case ... of (из первого примера)?
Впринципе можно было в случае добавления списка из новых функций, расширить код case of, и перечислить еще тридцать значений....
Но использование процедурных переменных, позволило бы нам занести в переменную, допустим F имя нужной функции, а затем использовать выражение следующего вида:
Код

  Result:=F(X);
Тогда как на самом деле функции F мы не объявляли, т е у нет кода
Код

Function F(...);
.....

Но у нас есть тип данных
Код

Type
  TFunct=Function(X:Real):Real;

и Переменная
Код

Var
  F:TFunct;

Которые не смотря на отсутствие функции F позволяют выполнить строку Result:=F(X);
Если перед этой строкой в F будет ссылка на функцию, извлекающую корень, то эта строка будет эквивалентна Result:=SQRT3(X); как будто вместо F стоит идентификатор SQRT3. А сохранять в переменной процедурного типа даных ссылку на процедуру очень просто - простым присваиванием:
Код

Type
   TFunct=Function(Var X:String):Real;
Function XYZ(Var X:String):Real;
Begin
...
End;
Var
   Ft:TFunct;
Begin 
  ....
  Ft:=XYZ;  // просто идентификатор функции, без скобок и параметров!!!
  ...


Теперь нескоко слов о специфике реализации таких функций:
все процедурыи функции, имена которых присваиваются процедурным переменным, необходимо компилировать с ключом компиляции $F+. Вложенные процедуры и функции с процедурными переменными использовать нельзя. Ну и самое обидное, это то что в качестве ссылок НЕМОГУТ быть использованы СТАНДАРТНЫЕ процедуры и функции типа sin, cos и т п. Последнее правда можно легко обойти: просто создать функцию, которая будет в качестве своей реализации использовать стандартную. Например, так может быть реализованы ссылки на тригонометрические функции:
Код

{$F+}  // включаем специальный режим компиляции
Function MySin(X:Real):Real;
Begin
  MySin:=Sin(X);
End;
Function MyCos(X:Real):Real;
Begin
  MyCos:=Cos(X);
End;
{$F-}  // выключаем...
Type
   TTrigFunct=Function(X:Real):Real;
Var
   F:TTrigFunct;
...
  F:=MyCos;
...
  Res:=F(0.5); // эквивалентно MyCos(0.5) или еще проще просто cos(0.5)
....


Добавлено @ 20:33
Ну есчо раз повторю, что процедурный тип данных ничем не отличается от других типов данных. После обявления соответствующего типа, можно использовать кроме переменных еще и константы процедурного типа (естно сама процедура должна быть описана "выше" чем объявлена константа).

Ну и теперь подитожу. Если в нашей первоначальной задаче, которую наверное уже все забыли, использовать процедурный тип данных, то вызов функции (это вся конструкция case of), будет заменена на строчку вида:
Result:=F(X);
и в случае добавления еще нескольких функций, мы лишь добавляем реализацию этих функций, ну и добавляем есчо определение, какую же функцию мы хотим вызвать и все. вышеуказанная строка, остаются и все будет работать.

Да замечу, что при использовании массива, эл-ты которого яв-ся переменными процедурного типа, вызов функции, которая назначана I-ому элементу будет выглядеть следующим образом:
Proc[I](X,Y,z)
Запись, которая может сбить с толку на первый взгляд. Но при боле внимательном рассмотрении все становится на свои места: Proc[I] - вызов эл-та массива Proc, имеющего номер I, а то что стоит дальше - скобки и параметры - это дальнейший вызов нужной нам функции, ведь элемент массива Proc[I] можно заменить обычной, не индексированной переменной, например A, тогда запись более понятна:
A(X,Y,Z)

И есчо, кто-то может сказать, что в случае использования этих самых процедурных переменных, программа только услажняется: надо описывать новый тп данных, использовать промежуточные переменные... Все же можно было написать аккуратно, и даже в нашей задаче, при добавлении новых возможностей легко можно было бы найти нужное место в программе и просто расширить список case of....
На такое я отвечаю тем, что массивы и записи, тоже тогда можно было бы отменить.... Написать просто 150 наименований разных переменных, вместо одного массива из 150 элементов, и радоваться жизни....

PS: а в некотрых случаях "системного" программирования без переменных процедурного типа просто не обойтись, ведь если решать задачу в общем виде, то скорее всего вы не знаете сразу как будет вычислять функция F(X,Y), а реализация этой функции будет известна только в прикладной программе. Так что если бы не было бы возможности ссылаться на другие, в том числе и внешние подпрограммы, наверное, невозможно было бы в системе windows, использование библиотек (в том числе и DLL), с доработаными или добавленными возможностями.
Все для начала я думаю хватит, а то меня дальше понесёт smile smile


PS: ЛЮДИ!!! откликнитесь насчет усвояемости и легкочитаемости...
ОЧЕНЬ НАДО!

Автор: Hidrag 16.4.2005, 21:24
SPrograMMer
Супер, учу паскаль но этим пользоваться не приходилось, даже когда нужно было!!!
Тебе учебники писать нужно! smile)) кстати про использованиее этого в DLLках в качестве второго примера можно чуть подробнее smile это более практичный и наглядный пример.

Автор: SPrograMMer 16.4.2005, 21:31
Цитата(Hidrag @ 16.4.2005, 21:24)
Тебе учебники писать нужно

А то! Программа обучения студентов "начала программирования в среде Delphi" моя работа, которую видел узкий круг человек (10-15) и прослушило этот курс всего-навсего 8 чел smile

Цитата(Hidrag @ 16.4.2005, 21:24)
кстати про использованиее этого в DLLках в качестве второго примера можно чуть подробнее  это более практичный и наглядный пример

буд сделано, тока чутка попозже, может даже к завтраму...

Автор: Fedor 16.4.2005, 21:33
Добавлено в FAQ. Еще раз огромное спасибо SPrograMMer! Так держать!
http://vingrad.ru/DELPHI-FAQ-002490

Автор: Fantasist 16.4.2005, 23:08
Прототип всех трех функций в примере лучше сделать одинаковым сразу.

Цитата(SPrograMMer @ 16.4.2005, 19:33)
прототип функции, (с появлением .Net их стали называть делегатами)


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

Цитата(SPrograMMer @ 16.4.2005, 17:31)
необходимо компилировать с ключом компиляции $F+


Если упоминается деректива компилятора, то неплохо было бы указать, что она делает. Я например такой никогда не пользовался и не видел, в Delphi7 я так же не смог отыскать ее в хелпе. Подозреваю, что ее больше не существует.


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


Добавлено @ 23:10
P.S. и удалите дубль пожалуйста.


Автор: SPrograMMer 17.4.2005, 08:03
Цитата(Fantasist @ 16.4.2005, 23:08)
Если упоминается деректива компилятора, то неплохо было бы указать, что она делает. Я например такой никогда не пользовался и не видел, в Delphi7 я так же не смог отыскать ее в хелпе. Подозреваю, что ее больше не существует.

Ну раздел хоть и называется "Delphi/Паскаль", подраздел то ведь - "Паскаль", поэтому я ориентировался на старые Dos - компиляторы, в том числе и на Borland Pascal 7.0...
Там все это есть.
Цитата

Режим компиляции $F+ включает компиляцию с расчетом на дальнюю модель вызова подпрограмм (дальная модель памяти). $F- соответственно выключает. Дирректива  Far указанная сразу после объявления подпрограммы так же компилирует подпрограмму с расчетом на дальнюю память

Да ты прав в Delphi (даже наверное с 1-ой) такого режима нет, там все сразу компилируется с расчетом на дальную модель памяти...

Цитата(Fantasist @ 16.4.2005, 23:08)
Сам пример, на мой взгляд, не очень удачный - сразу приходит в голову вопрос, что где-то теперь в другом месте есть case, который присваевает процедурной ссылке нужную процедуру, что естесственно не дает никаких удобств или преимуществ. Для демонстрации подобных вещей лучше использовать концепцию callback функций.

smile я сам понимал, что неуданый а другого ничего не шло в голову. Спасибо подсказал. Учтём.... smile

Цитата(Fantasist @ 16.4.2005, 23:08)
Еще неплохо было бы здесь же рассмотреть указатель на метод

Ну это уже ООП, как мне кажется совершенно другая темя...

Автор: Fantasist 17.4.2005, 08:19


Цитата(SPrograMMer @ 17.4.2005, 05:03)
Ну это уже ООП, как мне кажется совершенно другая темя...


Хорошо, согласен.


Цитата(SPrograMMer @ 17.4.2005, 05:03)
Режим компиляции $F+ включает компиляцию


Ах вон оно что. Ну тогда для всех компиляторов для Win32 это не нужно. В Win32 вся память Flat и 32 разрядная (то есть Far). Точно так же, как и модификатор Far.

smile

C уважением.

Автор: SPrograMMer 17.4.2005, 08:24
Значит счас примерчиков по на саставлю:
Цитата(Hidrag @ 16.4.2005, 21:24)
про использованиее этого в DLLках

Цитата(Fantasist @ 16.4.2005, 23:08)
лучше использовать концепцию callback функций

и можно фак обнавлять. Кстати это делать моно, или там усе железно: раз и все ни каких редактирований более?

Автор: Fedor 17.4.2005, 08:43
Цитата(SPrograMMer @ 17.4.2005, 08:24)
Кстати это делать моно

Можно.

Автор: SPrograMMer 17.4.2005, 14:35
В качестве примера рассмотрю изобретение велосипеда. smile
Допустим сочиняем мы контейнер для чего-то. Ну т е коллекцию, в Delphi уже есть готовые классы-коллекции, в Turbo Vision то ж есть такое счастье. ООП рассматривать здесь не буду, т к это уже другая тема.
Так вот пусть наша коллекция будет хранить любые объекты, это значит что нам по большому счету надо будет хранить указатель на объект. Объект у меня это не термин ООП, а даные, т е в качестве объекта может рассматриваться массив, целое число, строка, запись, другой тип данный. Кто говорит что для обозначения любого, что здесь перечислено, нельзя приментть слово "объект" пусть кинет в меня камень!
А сама коллекция будут организована в виде очереди (основанной на динамических переменных). И пусть мы все чего на создадим поместим в библиотеку (может даже и в DLL). И после будем использовать наши труды в других програмах, и даже другим програмистам дадим воспользоваться.
Ну так вот для реализации очереди я предлагаю использовать следующую структуру:
Код

Type
   PItem=^TItem;
   TItem=Record
     Data:Pointer;
     Next,Pred:PItem;  // пусть очередь у нас будет двунаправленная
  End;
Var
  First,Last:PItem;

Далее мы реализуем подпрограммки по работе с этой бедой, например, добавление элементов, удаление элементов, поиск элементов, упорядочивание элементов...
Насчет добавления и удаления я думаю ничего сложного нет - подпрограммки простые!
Вот как реализовать поиск?
Ну что начинаем с простого - пусть в нашей коллекции (и точка!), будут храниться только целые числа (точнее указатели на них). Тогда мы могли бы написать что-то вроде:
Код

Function Find(Chislo:Integer):PItem;  // возвращаться будет элемент коллекции или Nil если нет такового
  Var
    CurItem:PItem;
Begin
  CurItem:=First;
  While CurItem<>Nil Do Begin
     If Integer(CurItem^.Data^)=Chislo Then Begin  
        Find:=CurItem;
        Exit
     End;
     CurItem:=CurItem^.Next
  End;
  Find:=Nil
End;

Несложно! Условие, в котором производится приведение значения,хранимого по указателю Data к целому типу я думаю понятно.
А что будет, если у нас в коллекции будут строки? Нужно изменить тип входного параметра, и приводить нужно к другому типу данных.
А если другой программист или мы в программке, использующей эти коллекции, создадим ... ну например запись, и хранить в Data будем указатель на эту запись. Тогда для сравнения, мы должны будем не только привести data^ к типу апись, но и выбрать из этой записи поле по которому будем сравнивать. Например, у нас будет сисок работников, и искать мы будем по имени, то есть по полю FIO в одном случае, а в другом - по окладу..........................
А если.... А что .....
типов данных может быть бесчисленное множество. И как сравнивать текущий элемент с эталоном?
Вот тута мы и воспользуемся ссылкой на подпрограмму, которую кстати сами и напишем в каждом конкретном случае, а в подпрограмму Find будем передовать как эталон, так функцию, которая будет вычислять равны ли два значения, или не равны...
Так как в коллекции может храниться все, что угодно, то тип эталона я думаю ясен - это ссылка (указатель) на эталон, т е Pointer. Значит заголовок нашей процедурки будет следующий:
Код

Function Find(Etalon:Pointer;FunctForCompare:[b]тип_функции[/b]):PItem;
...

В реализации мало, что измениться, разве что поменяется немного вид сравнения....
Давайте подумаем, что это за тип_функции нам нужен (это и будет параметр процедурного типа). Ну во-первых это будет функция, кроме того она будет возвращать логическое знаяение: True - если элементы одинаковы, и False - в противном случае. В функции будет сравниваться два значения: эталон и текущие данные. Тип их (я уже не обясняю) Pointer.
Вот и получили следующий процедурный тип:
Код

Type
   TFunctForCompare=Function(Item1,Item2:Pointer):Boolean;

ну и объявление подпрограмки (если кто еще не понял):
Код

Function Find(Etalon:Pointer;FunctForCompare:FunctForCompare):PItem;
...
begin
  ...
       If FunctForCompare(Etalon,CurItem^.Data) Then     // Элементы равны?
  ...
End;

Как видите в этом случае мы вместо конкретно сравнения (например двух целых чисел), перешли к более общему: теперь процедуру сравнения мы оставляем на совести основной программы, которая будет использовать нашу коллекцию.
Согласитесь, что в каждой программе реализовывать свою коллекцию, с особыми подпрограммами сравнения, в первом случае для целых чисел, в другом для записи, в третьем ...
трудоёмко. Проще реализовать общий подход, а в главной программе только ссылаться на то что уже есть чуть-чуть "доработав" этот общий подход к конкретному случаю, например, для сравнения строк.
Добавлено @ 14:36
Если кто не понял, то в основной программе мы с вами написали что-то типа:
Код

{$F+}
Function CompInteger(Item1,Item2:Pointer):Boolean;  // сравнение двух целых чисел
Begin
   CompInteger:=Integer(Item1^)=Integer(Item2^)
End;
Function CompString(Item1,Item2:Pointer):Boolean;  // двух строк
Begin
   CompString:=String(Item1^)=String(Item2^)
End;
Function CompMyRecord(Item1,Item2:Pointer):Boolean; // двух записей "только что придуманых"
Begin
   CompMyRecord:=MyRecord(Item1^).Pole1=MyRecord(Item2^).Pole1
End;
{$F-}

А в критический момент использовать это все так:
Код

EtalonStr^:='Строка';
If  Find(EtalonStr,CompString)<>Nil Then 
   // в коллекции есть элемент "Строка"!!!

или так:
Код

EtalonInt^:=33;
If  Find(EtalonIntr,CompInteger)<>Nil Then 
   // в коллекции есть элемент со значением 33

Оказалось наверное не так сложно как казалось с первого взгляда.
Мы реализовали решение задачи в общем виде, а часть специфического решения, оставили на совести основной программы.


Теперь подпрограмма упорядочивания.
Собсно ничего сложного опять нет, составляем общий алгоритм (пусть будет пузырёк):
Код

Procedure Sort;
  Var
    CurItem1,CurItem2:PItem;
  Procedure SWAP; // обмен двух элементов местами...
  Begin                    // реализация - задание читающему  :) 
    ....
  End;
Begin
  CurItem1:=First;
  While CurItem1^.Next<>Nil Do Begin
    CurItem2:=CurItem1^.Next;
    While CurItem2<>Nil Do Begin
        If Элемент1 < Элемент2 Then      // вот тут и будем использовать процедурный тип данных
          Swap;    // обмен                        //
        CurItem2:=CurItem2^.next
    End;
    CurItem1:=CurItem1^.Next
  End
End;

Теперь Нам надо выяснить как относятся друг к другу два элемента:
-они равны;
-первый больше вторго;
-первый меньше второго.
задача непосредственного сравнения остаётся за основной подпрограммой, и в каждом конкретном случае будет своя.
Нам остаётся определить обявление функции - какие параметры, какого типа, что ф-ция будет возвращать. Сравниваем мы опять два элемента, поэтому с входными параметрами ясно. Результатов сравнения у нас три - использовать логический тип данных в принципе можно, но не желательно, давайте будет разнообразновать свою жизнь. smile
Поэтому пусть наша ф-ция возвращает целые числа:
0 - равные элементы;
-1 - первый больше вторго;
+1 - первый меньше второго.
соответственно тип такой функции следующий:
Код

Type
  TFunctForSort=Function(Item1,Item2:Pointer):Integer;

А дальше.... а дальше так же как и с подпрограммой поиска....
Добавлено @ 14:41
Таким образом можно придумать есчо кучу подпрограмм для работы с коллекцией, и со ссылками на другие подпрограммы.



ЗЫ: Опять целая статья получилась smile

Автор: Romtek 18.4.2005, 16:10
Хочу добавить (извините, если я повторяюсь), что:
если есть функции, у которых разные типы входных и возвращаемых переменных, то нельзя их объединять одним функциональным типом.
То есть, если
function SQRT3(x:Real):Real;
function SQR5(X:Real):Real;
function Symma(X:Real):Integer;

то можно объявить переменную
Var
F:TFunct;
для передачи её в качестве параметра для функций SQRT3 и SQR5, но она не подойдёт для Symma, т.к. возвращаемый тип отличается от типа TFunct.

И, вообще, нельзя реализовать шаблон в Турбо Паскале как в языке Си, когда можно определять функции с разными входными и возвращаемыми типами переменных (хотя в некоторых 32-разрядных копиляторах Паскаля это возможно).

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)