воскресенье, 25 марта 2012 г.

Анонимные методы в Delphi

Перевод из справочной системы Delphi

Как предполагает название, анонимный метод – это процедура или функция, с которой не связано имени. Анонимный метод обрабатывает блок кода как сущность, которая может быть присвоена переменной или использоваться в качестве параметра для передачи в другой метод. Вдобавок анонимный метод может ссылаться на переменные и связывать значения с переменными в контексте, где он объявлен. Анонимные методы могут объявляться и использоваться при помощи простого синтаксиса. Они схожи с замыканиями, определенными в других языках программирования.


Синтаксис

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

function MakeAdder(y: Integer): TFuncOfInt;
  begin
    Result := { начало анонимного метода } function(x: Integer) : Integer
      begin
        Result := x + y;
      end; { конец анонимного метода }
  end;

Функция MakeAdder возвращает фунцию, которую объявляет без имени как анонимный метод.

Следует учесть, что MakeAdder возвращает значение типа TFuncOfInt. Тип анонимного метода объявлен как ссылка на метод:

type
  TFuncOfInt = reference to function(x: Integer): Integer;
Это объявление указывает, что анонимный метод:
  • Является функцией;
  • Принимает целочисленный параметр;
  • Возвращает целочисленное значение.
Объявление типа для анонимной процедуры или функции:
type
  TType1 = reference to procedure (parameterlist);
  TType2 = reference to function (parameterlist): returntype;
где (parameterlist) является опциональным. Далее приведена еще пара примеров типов:
type
  TSimpleProcedure = reference to procedure;
  TSimpleFunction = reference to function(x: string): Integer; 
Анонимный метод объявлен как процедура или функция без имени:
// процедура
procedure (parameters)
begin
  { блок инструкций }
end;
// функция
function (parameters): returntype
begin
  { блок инструкций }
end;

где (parameters) является опциональным.


Применение анонимных методов

Обычно анонимные методы присваивают чему-либо, как это показано в примерах:

myFunc := function(x: Integer): string
begin
  Result := IntToStr(x);
end;

myProc := procedure(x: Integer)
begin
  Writeln(x);
end;

Анонимные методы могут также возвращаться функциями или передаваться в качестве параметров при вызове методов. Далее в качестве примера приведено применение переменной анонимного метода myFunc, объявленной выше:

type
  TFuncOfIntToString = reference to function(x: Integer): string;

procedure AnalyzeFunction(proc: TFuncOfIntToString);
begin
  { какой-то код }
end;

// вызов процедуры с параметром - анонимным методом
// через переменную:
AnalyzeFunction(myFunc);

// использование анонимного метода напрямую:
AnalyzeFunction(function(x: Integer): string
begin
  Result := IntToStr(x);
end;)

Ссылки на методы могут присваиваться как обычным методам, так и анонимным. Например:

type
  TMethRef = reference to procedure(x: Integer);
  TMyClass = class
    procedure Method(x: Integer);
  end;

var
  m: TMethRef;
  i: TMyClass;
begin
  // ...
  m := i.Method;   //присваивание значения ссылке на метод
end;

Тем не менее, обратное утверждение неверно: вы не можете присвоить значение типа анонимного метода обычному указателю на метод. Ссылки на методы являются управляемыми типами, а указатели на методы – неуправляемыми. То есть для обеспечения безопасности работы с типами присваивание ссылок на методы указателям на методы не поддерживается. Например, события – это свойства-указатели на методы, и вы не сможете использовать анонимные методы для работы с событиями. Для получения более подробной информации по этому ограничению – смотрите секцию о привязке переменных.


Привязка переменных в анонимных методах

Ключевая особенность анонимных методов заключается в том, что они могут обращаться к переменным, доступным в их области видимости. Более того, эти переменные могут быть привязаны к своим значениям и обернуты обращением к анонимному методу, что фиксирует состояние переменных и удлиняет время их существования.


Иллюстрация привязки переменных

Рассмотрим еще раз объявленную выше функцию:
function MakeAdder(y: Integer): TFuncOfInt;
begin
  Result := function(x: Integer): Integer
  begin
    Result := x + y;
  end;
end; 

Мы можем создать экземпляр этой функции, который привязывает значение переменной:

var
  adder: TFuncOfInt;
begin
  adder := MakeAdder(20);
  Writeln(adder(22)); // выводит 42
end.

Переменная adder хранит анонимный метод, который привязывае значение 20 к переменной y, к которой обращается блок кода анонимного метода. Эта привязка сохраняется даже в том случае, когда значение пропадает из области видимости.


Анонимные методы как события

Причина для использования ссылок на методы – возможность иметь тип, содержащий привязанные значения, которые также известные как значения замыкания. Поскольку замыкания закрывают контекст, в котором они определены, включая любые локальные переменные, на которые идет ссылка в точке объявления, у них наступает состояние, в котором их память должна быть освобождена. Ссылки на методы являются управляемыми типами (работают по принципу подсчета ссылок), они могут следить за этим состоянием и освобождать память, когда это необходимо. Если ссылка на метод или замыкание могли бы свободно присваиваться указателю на метод (например, событию), это привело бы к "подвисанию указателей" или утечкам памяти.

События в Delphi – это конвенция для свойств. Между событием и свойством нет разницы, кроме как в типе. Если свойство связано с указательным типом, оно является событием.

Если свойство имеет тип ссылки на метод, то с точки зрения логики, оно также должно рассматриваться как событие. Тем не менее, IDE не обрабатывает его как событие. Это утверждение относится к классам, установленным в IDE в качестве компонентов или элементов управления.

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

Далее приведен пример использования свойств типа ссылок на метод, который работает аналогично событию:

type
  TProc = reference to procedure;
  TMyComponent = class(TComponent)
  private
    FMyEvent: TProc;
  public
    // свойство MyEvent работает как событие:
    property MyEvent: TProc read FMyEvent write FMyEvent;
    // прочий код вызывает FMyEvent как обычное событие
  end;

...

var
  c: TMyComponent;
begin
  c := TMyComponent.Create(Self);
  c.MyEvent := procedure
  begin
    ShowMessage('Hello World!'); // показывается, когда 
    //TMyComponent вызывает MyEvent
  end;
end;

Механизм привязки переменных

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

Локальные переменные, объявленные в начале процедуры, функции или метода (в дальнейшем "подпрограммы") обычно живут, пока подпрограмма активна. Анонимные методы продлевают жизнь этих переменных.

Если анонимный метод в своем теле обращается к внешней локальной переменной, эта перменная "захватывается". Захват означает продление жизни переменной, то есть она сохраняется до тех пор, пока хранится значение анонимного метода и не погибает с подпрограммой, в которой она объявлена. Следует учесть, что захватываются переменные, но не их значения. Если значение переменной изменяется после захвата при создании экземпляра анонимного метода, значение переменной, которую захватил анонимный метод также меняется, поскольку эти переменные хранятся в одном и том же месте и являются одной переменной. Захваченные переменные хранятся в куче, а не в стеке.

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

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

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

Для примера рассмотрим ситуацию:
type
  TProc = reference to procedure;
procedure Call(proc: TProc);
// ...
procedure Use(x: Integer);
// ...

procedure L1; // рамка F1
var
  v1: Integer;

  procedure L2; // рамка F1_1
  begin
    Call(procedure // рамка F1_1_1
    begin
      Use(v1);
    end);
  end;

begin
  Call(procedure // рамка F1_2
  var
    v2: Integer;
  begin
    Use(v1);
    Call(procedure // рамка F1_2_1
    begin
      Use(v2);
    end);
  end);
end;

Чтобы определить, какой рамочный объект с чем связан, каждая подпрограмма и анонимный метод снабжается идентификатором рамки:

  • v1 это переменная в F1;
  • v2 это переменная в F1_2 (захватывается F1_2_1);
  • Анонимный метод для F1_1_1 – это метод в F1_1;
  • F1_1 свзявается с F1 (F1_1_1 использует v1);
  • Анонимный метод для F1_2 – это метод в F1;
  • Анонимный метод для F1_2_1 – это метод в F1_2;

Рамки F1_2_1 и F1_1_1 не нуждаются в рамочных объектах поскольку они не объявляют анонимных методов и не имеют захваченных переменных. Они не находятся на линии родства между встроенным анонимным методом и внешней захваченной переменной (у них есть явные рамки, которые хранятся в стеке).

При получении ссылки на анонимный метод F1_2_1 переменные v1 и v2 уже не уничтожаются. Если же, вместо этого, единственной оставшейся ссылкой после включения F1 будет F1_1_1, то не уничтожится только переменная v1.

В цепочках связей между ссылками на метод и рамками возможно создать цикл, который приведет к утечке памяти. Например, сохраняя анонимный метод напрямую (или не напрямую) в переменной, которую захватывает этот анонимный метод, можно создать цикл приводящий к утечке памяти.


Применение анонимных методов

Анонимные методы предлагают более чем простые указатели на все, что можно вызвать. Они предоставляют следующие преимущества:

  • Привязка значений переменных;
  • Простой способ определения и применения методов;
  • Простой способ параметризовать используемый код.

Привязка переменных

Анонимные методы предоставляют блок кода вместе с привязками переменных к окружению, в котором они определены, даже тогда, когда это окружение находится вне области видимости. Указатель на функцию или процедуру такого не может.

Например, инструкция adder := MakeAdder(20); из примера, приведенного выше производит переменную adder, которая захватывает привязку переменной к значению 20.

Некоторые другие языки, которые реализуют такие конструкции, обращаются к ним как к замыканиям. Исторически идея была в том, чтобы вычисление выражения вида adder := MakeAdder(20) создавало замыкание, представляющее собой объект, содержащий ссылки на привязки ко всем переменным, к которым происходит обращение в функции, а также к переменным, определенным снаружи этой функции. Что и замыкает его, захватывая значения переменных.


Простота в использовании

Следующий пример показывает типичное объявление класса и нескольких простых методов с последующим их вызовом:

type
  TMethodPointer = procedure of object; // delegate void TMethodPointer();
  TStringToInt = function(x: string): Integer of object;

TObj = class
  procedure HelloWorld;
  function GetLength(x: string): Integer;
end;

procedure TObj.HelloWorld;
  begin
    Writeln('Hello World');
  end;

function TObj.GetLength(x: string): Integer;
begin
  Result := Length(x);
end;

var
  x: TMethodPointer;
  y: TStringToInt;
  obj: TObj;

begin
  obj := TObj.Create;

  x := obj.HelloWorld;
  x;
  y := obj.GetLength;
  Writeln(y('foo'));
end.

Эти же методы вызываются с использованием анонимных методов:

type
  TSimpleProcedure = reference to procedure;
  TSimpleFunction = reference to function(x: string): Integer;

var
  x1: TSimpleProcedure;
  y1: TSimpleFunction;

begin
  x1 := procedure
    begin
      Writeln('Hello World');
    end;
  x1;   //вызов только что объявленного анонимного метода

  y1 := function(x: string): Integer
    begin
      Result := Length(x);
    end;
  Writeln(y1('bar'));
end.

Обратите внимание, насколько проще и короче код, который использует анонимные методы. Он идеален в том случае, если вы хотите явно и без сложностей объявить эти методы и использовать их немедленно, без затрат и усилий на создание класса, который, возможно, никогда не будет больше использован. Результирующий код проще для понимания.


Применение кода для передачи в качестве параметра

Анонимные методы упрощают написание функций и структур, управляемых передаваемым в качестве параметра кодом, а не просто значениями.

Многопоточность – это хорошее применение для анонимных методов. Если вы хотите выполнять параллельно какой-то код, возможно вам пригодится функция на подобие этой:

type
  TProcOfInteger = reference to procedure(x: Integer);

procedure ParallelFor(start, finish: Integer; proc: TProcOfInteger);

Процедура ParallelFor работает в разных потоках. Предположим, что эта процедура правильно реализована, эффективно использует потоки в потоковом пуле и может быть легко использована для получения преимущества при работе с несколькими процессорами:

procedure CalculateExpensiveThings;
var
  results: array of Integer;
begin
  SetLength(results, 100);
  ParallelFor(Low(results), High(results),
    procedure(i: Integer)                   // \
    begin                                   //  \ блок кода
      results[i] := ExpensiveCalculation(i);//  /  используется
    end                                     // /  как параметр
    );
  // пользуемся результатами
  end;

Как это могло быть реализовано без анонимных методов? Возможно при помощи класса-задачи с виртуальным абстрактным методом, для которого будет создан конкретный, не слишком естественный и интегрированный наследник, реализующий ExpensiveCalculation, и добавляющий все задания в очередь.

Здесь приведен "распараллеливаемый" алгоритм-абстракция, которая параметризуется кодом. В прошлом обычным решением для реализации такого паттерна было бы создание виртуального базового класса с одним или несколькими абстрактными методами (вспомним класс TThread и его абстрактный метод Execute). Как бы то ни было, анонимные методы позволяют сделать этот паттерн параметризуемым алгоритмами и упростить структуры данных, использующие код.

Комментариев нет:

Отправить комментарий