無名メソッドとスレッド

By: Yusuke Konno

Abstract: Delphi 2009で新たに搭載された言語機能である無名メソッドの具体的な活用例を紹介します。

Delphi 2009ではUnicode化が大きなトピックですが、同時に言語機能の強化も行われています。無名メソッド(匿名メソッド、Anonymous Method)はその1つです。無名メソッドは名前の通り名前のない関数・手続きで、これを利用することでクロージャーの実現や、高階関数の実現などが可能になります。

しかし、クロージャーや高階関数はDelphiユーザーにとってはそれほど馴染みのあるものではありません。しかし、無名メソッドはスレッドで用いるケースがあります。今回は無名メソッドをスレッド内で利用するという最も身近な活用方法について解説を行います。

    スレッドにおける従来のコード(Delphi 2007まで)

    Synchronizeメソッド

ワーカースレッドの処理中にメインスレッドのコンポーネント等にアクセスしたい時は、Synchronizeメソッドを使う必要があります。なぜなら、VCLのビジュアルコンポーネントはメインスレッドからアクセスしなければならないからです。Synchronizeメソッドを使わずにワーカースレッド内からメインスレッドのコンポーネント等にアクセスしようとすると、複数のスレッドから同時にアクセスされて、コンポーネントを破壊してしまう可能性があります。

以下の例は従来のスレッドにおけるSynchronizeメソッドを用いたコードです。

スレッドオブジェクトのユニット:OldThread.pas

unit OldThread;

interface

uses
  Classes, SysUtils;

type
  TOldThread = class(TThread)
  private
    { Private 宣言 }
    FResultInt: Integer;//Synchronize時に必要なフィールド
  protected
    procedure Execute; override;
    procedure SyncMethod;
  end;

implementation

uses Unit1;//メインフォームのユニット

{ TOldThread }

procedure TOldThread.Execute;
var
  i: Integer;
  A,B: Integer;
begin
  { スレッドとして実行したいコードをここに記述してください }
  for i := 0 to 99 do
  begin
    A := Random(10000);
    B := Random(10000);
    FResultInt := A - B;
    Synchronize(SyncMethod);//直接A-Bの計算結果を渡せない!!
  end;
end;

procedure TOldThread.SyncMethod;
begin
  //Sync
  Form1.Memo1.Lines.Add('OT : '+FormatFloat('#,##0',FResultInt));
end;

end.

メインフォームのユニット:Unit1.pas(一部)

uses
  ....., OldThread;

type
  TForm1 = class(TForm)
    { 略 }
  public
    { Public 宣言 }
    OT: TOldThread;//スレッドオブジェクト
    procedure EndOT(Sender: TObject);//OnTerminateイベント用
  end;

{略}

procedure TForm1.Button1Click(Sender: TObject);
begin
  //OldThread
  Memo1.Clear;
  Button1.Enabled := False;
  OT := TOldThread.Create(False);
  OT.FreeOnTerminate := True;
  OT.OnTerminate := EndOT;
end;

procedure TForm1.EndOT(Sender: TObject);
begin
  //OldThread終了時
  Button1.Enabled := True;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //FormCreate
  Randomize;
end;

実行結果

Hide image
am_thread_001

AとBに0から9999のランダムな整数値を代入し、A-Bという計算を行い、その結果をForm1のMemoコンポーネントに追加という作業を100回繰り返しています。

    Synchronizeメソッドの問題点

Synchronizeメソッドの引数に渡す手続きは、何も引数を取ることが出来ないという制限があります。Memoコンポーネントはメインスレッド上にあるため、計算結果の追加に際してはSynchronizeメソッドを使わなければなりませんが、上記制限によってExecuteメソッド内のローカル変数を直接Synchronizeメソッドの引数に指定した手続きに渡すことが出来ません。これを解決するため、通常はスレッドクラスにプライベートフィールドを宣言し、そこに計算結果を保存することでSynchronizeメソッドの引数に指定した手続きから参照できるようになります。

上記コードではprivateスコープにFResultIntというInteger型の変数を媒介にして、SyncMethodにA-Bの計算結果を渡しています。この例では追加するフィールドが1つであるため、さして問題がないようにも思えますが、渡したいデータが増加するとその分フィールドを増やさなければならないため、コードが分かり辛くなります。また、そもそもFResultIntの存在自体が無駄でもあります。

    無名メソッドを使う方法

    無名メソッドとは?

無名メソッドはDelphi 2009で導入された言語機能の1つです。無名メソッドは以下のように型宣言することができます。

type
  TAnonymousMethod = reference to procedure

なにやら見慣れない型宣言ですね。これだけでは分かり辛いので、実際に変数として宣言して使用した例を見てみましょう。

type
  TIntAM = reference to procedure (n: Integer);
var
  IAM: TIntAM;
begin
  IAM := procedure (n: Integer)
    begin
      ShowMessage(FormatFloat('#,##0',n));
    end;
  IAM(10);
  IAM(999);
  IAM($FFFF);
end;

TIntAMという無名メソッドの型を宣言し、IAMという変数を宣言しています。IAMには手続きの実体を代入しています。手続きの実体は代入時にそのまま記述してしまっているのが従来のDelphiコードにはみられない特徴です。

変数IAMに手続きの実体を代入した後は、実行することが可能です。IAM(10)、IAM(999)、IAM($FFFF)が実行している部分です。それぞれ、10、999、65,535という数字がメッセージボックスとして表示されます。なお、無名メソッドの引数には定数だけではなく変数も渡すことが可能です。しかも、無名メソッドが定義されているスコープ内で宣言されているローカル変数も引数として渡すことが可能です。

    Synchronizeと無名メソッド

Delphi 2009のスレッドは、Synchronizeメソッドの引数に無名メソッドを指定することが可能です。無名メソッドを利用することでSynchronizeの問題点を解決することが出来ます。無名メソッドを利用した具体的なコードを以下に示します。

スレッドオブジェクトのユニット:NewThread.pas

unit NewThread;

interface

uses
  Classes, SysUtils;

type
  TNewThread = class(TThread)
  private
    { Private 宣言 }
  protected
    procedure Execute; override;
  end;

implementation

uses Unit1;//メインフォームのユニット

{ TNewThread }

procedure TNewThread.Execute;
var
  i: Integer;
  A,B: Integer;
  Sync: TThreadProcedure;
begin
  { スレッドとして実行したいコードをここに記述してください }
  Sync := procedure
    begin
      Form1.Memo1.Lines.Add('NT : ' + FormatFloat('#,##0',A-B));
    end;
  for i := 0 to 99 do
  begin
    A := Random(10000);
    B := Random(10000);
    Synchronize(Sync);
  end;
end;

end.

メインフォームのユニット:Unit1.pas(一部)

uses
  ......, NewThread;

type
  TForm1 = class(TForm)
    { 略 }
  public
    { Public 宣言 }
    NT: TNewThread;//スレッドオブジェクト
    procedure EndNT(Sender: TObject);//OnTerminate用
  end;

{略}

procedure TForm1.Button2Click(Sender: TObject);
begin
  //NewThread
  Memo1.Clear;
  Button2.Enabled := False;
  NT := TNewThread.Create(False);
  NT.FreeOnTerminate := True;
  NT.OnTerminate := EndNT;
end;

procedure TForm1.EndNT(Sender: TObject);
begin
  //NewThread終了時
  Button2.Enabled := True;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //FormCreate
  Randomize;
end;

先ほどのOldThread.pasに記述されていたコードとは何点か違いがあることが分かると思います。まず、Synchronizeメソッドの引数に無名メソッド変数を指定しているため、Synchronizeメソッドの引数に渡していた手続きが無くなっています。また、媒介用のプライベートフィールドも存在しません。

コードの解説をすると、Executeメソッドではまず、TThreadProcedure型の変数Syncに無名メソッドの手続きの実体を渡しています。TThreadProcedure型はClassesユニットで宣言されています(実体はTThreadProcedure = reference to procedureとなっているだけです。)これが、Synchronizeメソッドの引数に渡されるため、OldThread.pasにあったSyncMethodと同じ役割を果たします。注目すべきは、SyncMethodではExecuteメソッドのローカル変数であるAとBが直接利用できなかったために、媒介としてクラスにInteger型のフィールドであるFResultIntを用意する必要がありましたが、無名メソッドを用いるとExecuteメソッドのローカル変数を無名メソッド内で利用できるため、媒介用のフィールドが必要なくなるのです。後はAとBにランダムで整数値を代入し、Synchronizeメソッドに先ほどの無名メソッド変数を代入しています。このコードを実行すると以下のようになります。

実行結果

Hide image
am_thread_002

ちなみに無名メソッド変数をいちいち用意しなくても、以下のようにSynchronizeメソッドの引数部分に直接無名メソッドを記述してもOKです。

procedure TNewThread.Execute;
var
  i: Integer;
  A,B: Integer;
begin
  { スレッドとして実行したいコードをここに記述してください }
  for i := 0 to 99 do
  begin
    A := Random(10000);
    B := Random(10000);
    Synchronize(procedure
      begin
        Form1.Memo1.Lines.Add('NT : ' + FormatFloat('#,##0',A-B));
      end);
  end;
end;

上記コードでは無名メソッド変数の宣言を省くことが出来るという利点がありますが、若干読みづらいかもしれません。注意点として、無名メソッドのendのあとにセミコロン(;)を付けてはいけません。

当然ですがDelphi 2009でも従来のコードは動作します。しかし、無名メソッドを利用することでも、従来のコード同等の結果を得ることが出来ました。しかも、余計なフィールド利用を省くことまでも出来ています。無名メソッドは速度的に若干のペナルティがあり、少々特殊な構文なので読みにくいという欠点がありますが、利用価値は十分にあると思うので、是非利用してみてください。

Server Response from: ETNASC01