是否有可能捕获Delphi匿名方法/闭包的局部变量值?

时间:2015-07-02 20:20:32

标签: delphi closures anonymous-methods

Delphi实现匿名方法的一个有趣方面 / closures是捕获例程本地变量状态的能力 从中调用anon方法。这在Marco Cantu的书中有很好的描述(Delphi 2009?)。

我的问题是,有没有办法同样捕获变量的值 哪些是匿名方法本身的本地?

为了说明我的意思,请考虑以下代码段, 哪个做我的意思:

type

  TMemoProc = reference to procedure(Memo: TMemo; StepNo : Integer);

  TForm1 = class(TForm)
  [...]
  private
    procedure Process(Memo: TMemo; StepNo: Integer);
  public
    { Public declarations }
    MemoProc : TMemoProc;
    procedure CreateMemoProc;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
begin
  for i := 1 to 3 do
    MemoProc(Memo1, i);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  CreateMemoProc;
  MemoProc(Memo1, 0);
end;

procedure TForm1.Process(Memo : TMemo; StepNo : Integer);
var
  Count : Integer;
begin
  if StepNo = 0 then
    Count := 1
  else
    Inc(Count);
  Memo.Lines.Add(Self.Name + ' ' +Format('stepno: %d, count: %d', [StepNo, Count]));
end;

procedure TForm1.CreateMemoProc;
begin
  MemoProc :=
    procedure (Memo : TMemo; StepNo : Integer)
    var
      Count : Integer;
    begin
      if StepNo = 0 then
        Count := 1
      else
        Inc(Count);
      Memo.Lines.Add(Format('stepno: %d, count: %d', [StepNo, Count]));
    end;
end;

单击Button1几次,插入的数字也就不足为奇了 改变,正如你所期望的那样,事实上Delphi没有隐式初始化 堆栈上的简单变量。

但我想知道是否有人可以想到一种巧妙的方法 在它的调用之间捕获anon方法的局部变量。

Fwiw,我之所以想知道这是因为有用例 通常需要对anon方法进行两次调用,一次要做一些 初始化和一个迭代,以及最简洁的编码方式 如果anon方法的局部变量可能以某种方式被捕获。

我想到的一种用例是编写一些通用代码来迭代数据集,在这里可以方便地指定一个布尔函数,该函数通过引用行的字段值来确定特定数据集行是否应该是处理和另一个定义当布尔函数返回true时应该对该行做什么。

在传统实现中,可以将字段指定为DataSet.FieldByName('xxx'),但是涉及许多字段和/或许多行时,这可能非常低效。

因此,我的q背后的议程基本上是能够指定anon方法的初始化调用中涉及的字段,然后直接使用它们(如DataSet11FieldCatflap,而不是DataSet.FieldByName('Catflap')。我已经考虑使用容器化的字段列表在初始化调用和迭代调用之间进行,但它似乎对我“不满意”。

2 个答案:

答案 0 :(得分:2)

唯一可以捕获变量的是匿名方法。您希望有两个捕获相同变量的匿名方法。这意味着您需要在单个方法中创建匿名方法,以便匿名方法可以捕获相同的局部变量。像这样:

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

procedure PerformCapture(InitialValue: Integer; out Getter: TFunc<Integer>;
  out Setter: TProc<Integer>);
var
  Value: Integer;
begin
  Value := InitialValue;

  Getter := function: Integer
    begin
      Result := Value;
    end;

  Setter := procedure(AValue: Integer)
    begin
      Value := AValue;
    end;
end;

var
 Getter: TFunc<Integer>;
 Setter: TProc<Integer>;

begin
  PerformCapture(42, Getter, Setter);
  Writeln(Getter());
  Setter(666);
  Writeln(Getter());
  Setter(-17);
  Writeln(Getter());
  Readln;
end.

<强>输出

42
666
-17

显然我已经编写了非常简单的匿名方法作为GetterSetter返回,但你可以自由地编写任意复杂的方法。实际上,您不需要直接在GetterSetter匿名方法中实现所有内容。您可以将方法转发到代码的其他部分,传递捕获的变量。

匿名方法非常适合简单的状态捕获。但是,您不必使用匿名方法来将状态从一个方法调用传递到下一个方法。您可以使用更传统的方法,例如包含状态的类。您可能会比较这两种可能的方法,以便选择导致更清晰编码的机制。

答案 1 :(得分:2)

匿名方法从其封闭范围捕获局部变量的状态。如果方法的实现需要更多状态,则在封闭范围内声明更多局部变量,即使它们仅在匿名方法的范围内使用过。

在您的情况下,这意味着在Count方法中声明和初始化CreateMemoProc,而不是在MemoProc中存储的匿名方法内。

一般来说,将变量声称看似错误的范围可能会造成难看和混乱。为避免这种情况,您可以使用方法工厂,其唯一的工作是生成匿名方法实例。那么关于变量声明的地方的任何奇怪都限于那个函数,所以目的很明确。你已经在CreateMemoProc已经有了这个,但是如果它取决于我,它将是一个独立的函数而不是形式的方法:

function CreateMemoProc: TMemoProc;
var
  Count: Integer;
begin
  Count := 0;
  Result := procedure (Memo : TMemo; StepNo : Integer)
    begin
      if StepNo = 0 then
        Count := 1
      else
        Inc(Count);
      Memo.Lines.Add(Format('stepno: %d, count: %d', [StepNo, Count]));
    end;
end;

Count变量是CreateMemoProc的本地变量,它在匿名方法中使用,匿名方法捕获它。 CreateMemoProc的每次调用都会创建Count的新副本,因此每个匿名方法都会知道它被调用了多少次。

但我觉得您并不想在方法中包含StepNo参数。相反,您希望该方法“知道”是否已经调用它。在这种情况下,我们可以使用标志增加Count

type
  TMemoProc = reference to procedure(Memo: TMemo);

function CreateMemoProc: TMemoProc;
var
  Count: Integer;
  Initialized: Boolean;
begin
  Count := 0;
  Initialized := False;
  Result := procedure(Memo: TMemo)
    begin
      if not Initialized then begin
        Count := 0;
        Initialized := True;
      end;
      Inc(Count);
      Memo.Lines.Add(Format('count: %d', [Count]));
    end;
end;