【问题标题】:How can I capture variables by anonymous method when using it in OTL?在OTL中使用匿名方法如何捕获变量?
【发布时间】:2012-11-01 04:29:15
【问题描述】:

我想做什么:

我在一个通用列表中有一些对象。我想在匿名方法中捕获每个对象,并将该方法作为单独的 OTL 任务执行。

这是一个简化的例子:

program Project51;

{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections, OtlTaskControl, OtlTask;

type
  TProc = reference to procedure;

type
  TMyObject = class(TObject)
  public
    ID: Integer;
  constructor Create(AID: Integer);
  end;

constructor TMyObject.Create(AID: Integer);
begin
  ID := AID;
end;

var
  Objects: TList<TMyObject>;
  LObject: TMyObject;
  MyProc: TProc;
begin
  Objects := TList<TMyObject>.Create;
  Objects.Add(TMyObject.Create(1));
  Objects.Add(TMyObject.Create(2));
  Objects.Add(TMyObject.Create(3));
  for LObject in Objects do
  begin
    //This seems to work
    MyProc := procedure
    begin
      Writeln(Format('[SameThread] Object ID: %d',[LObject.ID]));
    end;
    MyProc;
    //This doesn't work, sometimes it returns 4 lines in console!?
    CreateTask(
      procedure(const Task: IOmniTask)
      begin
        Writeln(Format('[Thread %d] Object ID: %d',[Task.UniqueID, LObject.ID]));
      end
    ).Unobserved.Run;
  end;
  Sleep(500); //Just wait a bit for tasks to finish
  Readln;
end.

结果如下:

如您所见,捕获似乎在主线程中运行良好。但是,我不知道是捕获了指向对象的指针还是仅捕获了它的ID字段?

当我尝试捕获对象并将匿名方法传递给CreateTask 函数时,事情变得很奇怪。

首先,似乎只捕获了TMyObject 的第三个实例。其次,尽管我在通用列表中只有三个对象,但我在控制台日志中有四条消息。第二种行为不一致,有时我在控制台中收到三个消息,有时我收到四个。

请解释我上面提到的两个问题的原因,并提出一个解决方案来消除这个问题,并允许我将每个对象实例传递给一个单独的 OTL 任务。 (我不想使用常规的TThread 类。)

【问题讨论】:

    标签: delphi closures delphi-2009 anonymous-methods omnithreadlibrary


    【解决方案1】:

    The documentation describes what's happening:

    请注意,变量捕获捕获的是变量,而不是。如果通过构造匿名方法捕获变量后,变量的值发生变化,匿名方法捕获的变量的值也会发生变化,因为它们是同一个变量,具有相同的存储空间。

    在你的代码中,只有一个LObject 变量,所以所有你构造的匿名方法都会引用它。随着循环的进行,LObject 的值会发生变化。这些任务还没有机会开始运行,所以当它们最终运行时,循环已经终止并且LObject 具有它的最终值。形式上,最终值在循环之后是未定义的。

    要捕获循环变量的,请将任务的创建包装在一个单独的函数中:

    function CreateItemTask(Obj: TMyObject): TOmniTaskDelegate;
    begin
      Result := procedure(const Task: IOmniTask)
                begin
                  Writeln(Format('[Thread %d] Object ID: %d',[Task.UniqueID, Obj.ID]));
                end;
    end;
    

    然后改变你的循环代码:

    CreateTask(CreateItemTask(LObject)).Unobserved.Run;
    

    【讨论】:

    • 谢谢Rob,我真正要捕获的(可能我不够清楚)是内存的地址,在这个地址下可以找到TMyObject的实例,因为我要使用方法和属性这个对象,不仅是它的 ID。但是修改你的例子应该不难吧?
    • 一点也不难;请参阅编辑后的答案。要按值而不是按引用捕获,请将所需的值传递给方法引用创建函数。在您的情况下,您希望捕获的值是存储在 LObject 中的对象引用。
    【解决方案2】:

    匿名过程捕获变量而不是值。因此,您正在捕获变量 LObject。由于这是一个循环变量,因此 LObject 的值会发生变化。匿名过程在执行时而不是在创建匿名过程时评估 LObject。

    我可能只使用 TMyObject 的方法,而不是使用匿名过程。尝试以这种方式编写代码,我预测您会发现它更容易理解。

    procedure TMyObject.TaskProc(const Task: IOmniTask);
    begin
      Writeln(Format('[Thread %d] Object ID: %d', [Task.UniqueID, Self.ID]));
    end;
    

    得到 4 行而不是 3 行输出的原因可能只是 WriteLn 不是线程安全的。将对 WriteLn 的调用封装在一个锁中以清除它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-03-09
      • 2010-10-29
      • 2011-07-06
      • 1970-01-01
      • 1970-01-01
      • 2013-08-04
      相关资源
      最近更新 更多