在使用OTL时,我如何通过匿名方法捕获变量?

6

我想做的事情:

我有一个通用列表中的几个对象。我想在匿名方法中捕获每个对象,并将此方法作为单独的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

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

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

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

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

2个回答

6

文档描述了正在发生的事情

请注意,变量捕获会捕获变量,而不是。如果变量在构造匿名方法后更改其值,则匿名方法捕获的变量的值也会更改,因为它们是具有相同存储的相同变量。

在您的代码中,只有一个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。但是修改您的示例不应该很难,对吧? - Wodzu
非常简单;请查看编辑后的答案。要通过值来捕获,而不是通过引用,请将所需的值传递给创建方法引用的函数。在您的情况下,您希望捕获的值是存储在LObject中的对象引用。 - Rob Kennedy

1

匿名过程捕获变量而不是值。因此,您正在捕获变量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的调用包装在锁定中以消除这个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接