Delphi での無名メソッド

提供: RAD Studio
移動先: 案内検索

手続きと関数:インデックス への移動

名前のとおり、無名メソッドとは、名前が付いていないプロシージャや関数のことです。無名メソッドでは、変数に代入したりメソッドへのパラメータとして使用できるエンティティとしてコード ブロックを扱います。さらに、無名メソッドは、自らが定義されるコンテキストで変数を参照したりそれらの変数に値をバインドすることもできます。無名メソッドは、簡単な構文で定義および使用することができます。 これらは、他の言語に定義されているクロージャの構文要素と似ています。

構文

無名メソッドは、通常のプロシージャや関数と同様に定義されますが、名前がありません。たとえば、以下の関数は、無名メソッドとして定義される関数を返します。


function MakeAdder(y: Integer): TFuncOfInt;
begin
Result := { 無名メソッドの定義開始 } function(x: Integer) : Integer
  begin
    Result := x + y;
    end; { 無名メソッドの定義終了 }
end;

関数 MakeAdder は、宣言時の名前がない関数つまり無名メソッドを返します。

なお、MakeAdderTFuncOfInt 型の値を返します。無名メソッドの型は、以下のように、メソッドへの参照として宣言されます。

type
  TFuncOfInt = reference to function(x: Integer): Integer;

この宣言は、この無名メソッドの以下の特徴を示しています。

  • 関数である
  • 1 つの整数型パラメータを取る
  • 整数値を返す

一般に、無名関数の型は、以下のように、プロシージャか関数のどちらかに宣言されます。

type
  TType1 = reference to procedure[(parameterlist)];
  TType2 = reference to function[(parameterlist)]: returntype;

型の例をいくつか以下に示します。


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

無名メソッドは、以下のように、名前のないプロシージャまたは関数として宣言されます。


// プロシージャの場合
procedure[(parameters)]
begin
  { ステートメント ブロック }
end;
// 関数の場合
function[(parameters)]: returntype
begin
  { ステートメント ブロック }
end;

無名メソッドの使用

無名メソッドは通常、以下の例に示すように、何かに代入されます。


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 には無名メソッドが含まれており、その無名メソッドは自らのコード ブロックで参照されている変数 y に値 20 をバインドします。このバインディングは、値がスコープから外れても存続します。

イベントとしての無名メソッド

メソッド参照を使用する動機は、バインドされた変数を中に含むことができる型(クロージャ値とも呼ばれます)を用意することです。クロージャはその定義環境(定義時点で参照されているローカル変数など)を中に閉じ込めているので、解放しなければならない状態を持っています。メソッド参照はマネージ型である(参照カウントの対象になる)ため、この状態を追跡でき、必要な場合には解放できます。メソッド参照やクロージャをメソッド ポインタ(たとえばイベントなど)に自由に代入できるようでは、ダングリング ポインタやメモリ リークのある型の不正なプログラムをたやすく作成できてしまいます。

Delphi イベントはプロパティの 1 つの規約です。型の種類を除き、イベントとプロパティには違いがありません。プロパティがメソッド ポインタ型であれば、それはイベントです。

プロパティがメソッド参照型であれば、それは論理的にはイベントでもあると考えなければなりません。ただし、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 に初期化される)かファイナライズされると、それによって捕捉されていた変数は最終的にスコープから出ます。

複数の無名メソッドが同じローカル変数を捕捉する場合、この状況はより複雑になります。あらゆる状況でこれがどのように動作するかを理解するには、実装の方法をより的確に把握しておく必要があります。

ローカル変数は、捕捉されるたびに、宣言元のルーチンに関連付けられている "フレーム オブジェクト" に追加されます。ルーチンで宣言されている無名メソッドはすべて、自らが含まれているルーチンに関連付けられているフレーム オブジェクトのメソッドに変換されます。最後に、無名メソッド値の生成や変数の捕捉によって作成されたフレーム オブジェクトが存在し、捕捉された外部変数にアクセスする必要がある場合、そのようなフレームはどれも別の参照によって親にリンクされます。1 つのフレーム オブジェクトからその親へのこうしたリンクもまた、参照カウントの対象になります。ネストしたローカル ルーチン内に宣言され親ルーチンの変数を捕捉する無名メソッドは、その親フレーム オブジェクトそのものがスコープから出るまで、それを有効な状態に保ちます。

たとえば、次のような状況を考えてみましょう。


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); というステートメントでは、値 20 への変数のバインディングをカプセル化する変数 adder ができます。

このような構文要素を実装している他のいくつかの言語では、これらをクロージャと呼んでいます。従来、これは、adder := MakeAdder(20); のような式を評価するとクロージャができるという考え方でした。これは、関数外で定義され関数内で参照されているすべての変数のバインディングへの参照を内部に含んだオブジェクトを表しており、そのため、それらの変数の値を捕捉することで閉じることができます。

使いやすさ

以下のサンプルでは、簡単なメソッドをいくつか定義してそれらを呼び出す典型的なクラス定義を示します。


type
  TMethodPointer = procedure of object; // 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.

無名メソッドを使用したコードの方がいかに簡単で短いかに注意してください。他ではまず使用しないようなクラスを作成するオーバーヘッドと労力をかけずに、これらのメソッドを明示的かつ簡単に定義してすぐに使用したいという場合には、これは理想的です。結果的に、わかりやすいコードになります。

パラメータとしてのコードの使用

無名メソッドを使用すると、値だけでなく、コードでパラメータ化された関数や構造を作成しやすくなります。

マルチスレッド処理は無名メソッドの格好のアプリケーションです。何らかのコードを並行に実行する場合は、たとえば以下のような並列 for 関数を使用することになるでしょう。

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 の具象下位クラスを 1 つずつ持つ "タスク" クラスを用意した後、すべてのタスクをキューに追加することになるでしょうが、その方法は自然とはとても言えず、統合されているとも言えません。

ここで、"並列 for" アルゴリズムは、コードでパラメータ化されている抽象概念になります。これまでは、1 つ以上の抽象メソッドを持つ仮想基底クラスを使用するのが、このようなパターンを実装する一般的な方法でした。TThread クラスとその抽象メソッド Execute に注目してください。しかし、無名メソッドを使用すると、このようなパターン(コードを用いたアルゴリズムやデータ構造のパラメータ化)ははるかに容易になります。