Delphi での無名メソッド
手続きと関数:インデックス への移動
名前のとおり、無名メソッドとは、名前が付いていない手続きや関数のことです。 無名メソッドでは、変数に代入したりメソッドへのパラメータとして使用できるエンティティとしてコード ブロックを扱います。 さらに、無名メソッドは、自らが定義されるコンテキストで変数を参照したりそれらの変数に値をバインドすることもできます。 無名メソッドは、簡単な構文で定義および使用することができます。 これらは、他の言語に定義されているクロージャの構文要素と似ています。
目次
構文
無名メソッドは、通常の手続きや関数と同様に定義されますが、名前がありません。
たとえば、以下の関数は、無名メソッドとして定義される関数を返します。
function MakeAdder(y: Integer): TFuncOfInt;
begin
Result := { start anonymous method } function(x: Integer) : Integer
begin
Result := x + y;
end; { end anonymous method }
end;
関数 MakeAdder は、宣言時の名前がない関数つまり無名メソッドを返します。
なお、MakeAdder は TFuncOfInt 型の値を返します。 無名メソッドの型は、以下のように、メソッドへの参照として宣言されます。
type
TFuncOfInt = reference to function(x: Integer): Integer;
この宣言は、この無名メソッドの以下の特徴を示しています。
- 関数である
- 1 つの整数型パラメータを取る
- 整数値を返す
一般に、無名関数の型は、以下のように、手続きか関数のどちらかに宣言されます。
type
TType1 = reference to procedure (parameterlist);
TType2 = reference to function (parameterlist): returntype;
ここで、(parameterlist) は任意指定です。
型の例をいくつか以下に示します。
type
TSimpleProcedure = reference to procedure;
TSimpleFunction = reference to function(x: string): Integer;
無名メソッドは、以下のように、名前のない手続きまたは関数として宣言されます。
// Procedure
procedure (parameters)
begin
{ statement block }
end;
// Function
function (parameters): returntype
begin
{ statement block }
end;
ここで、(parameters) は任意指定です。
無名メソッドの使用
無名メソッドは通常、以下の例に示すように、何かに代入されます。
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
{ some code }
end;
// Call procedure with anonymous method as parameter
// Using variable:
AnalyzeFunction(myFunc);
// Use anonymous method directly:
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; //assigning to method reference
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)); // prints 42
end.
変数 adder には無名メソッドが含まれており、その無名メソッドは自らのコード ブロックで参照されている変数 y に値 20 をバインドします。 このバインディングは、値がスコープから外れても存続します。
イベントとしての無名メソッド
メソッド参照を使用する動機は、バインドされた変数を中に含むことができる型(クロージャ値とも呼ばれます)を用意することです。 クロージャはその定義環境(定義時点で参照されているローカル変数など)を中に閉じ込めているので、解放しなければならない状態を持っています。 メソッド参照はマネージ型である(参照カウントの対象になる)ため、この状態を追跡でき、必要な場合には解放できます。 メソッド参照やクロージャをメソッド ポインタ(たとえばイベントなど)に自由に代入できるようでは、ダングリング ポインタやメモリ リークのある型の不正なプログラムをたやすく作成できてしまいます。
Delphi イベントはプロパティの 1 つの規約です。 型の種類を除き、イベントとプロパティには違いがありません。 プロパティがメソッド ポインタ型であれば、それはイベントです。
プロパティがメソッド参照型であれば、それは論理的にはイベントでもあると考えなければなりません。 ただし、IDE ではそれをイベントとして扱いません。 このことは、コンポーネントやカスタム コントロールとして IDE にインストールされるクラスの場合には、重要です。
したがって、コンポーネントやカスタム コントロールに関するイベントにメソッド参照やクロージャ値を使って代入できるようにするには、プロパティはメソッド参照型でなければなりません。 ただし、IDE ではそれをイベントとして認識しないため、これでは不都合です。
ここで、メソッド参照型プロパティ(したがって、イベントとして操作可能)の使用例を示します。
type
TProc = reference to procedure;
TMyComponent = class(TComponent)
private
FMyEvent: TProc;
public
// MyEvent property serves as an event:
property MyEvent: TProc read FMyEvent write FMyEvent;
// some other code invokes FMyEvent as usual pattern for events
end;
// …
var
c: TMyComponent;
begin
c := TMyComponent.Create(Self);
c.MyEvent := procedure
begin
ShowMessage('Hello World!'); // shown when TMyComponent invokes MyEvent
end;
end;
変数バインディングのメカニズム
メモリ リークの発生を避けるには、変数バインディング プロセスの詳細を理解することが有効です。
手続き、関数、あるいはメソッド(以下、"ルーチン")の開始時に定義されたローカル変数は通常、そのルーチンがアクティブな間だけ有効です。 無名メソッドを用いると、これらの変数の有効期間を延長することができます。
無名メソッドが外部のローカル変数を本体で参照する場合、その変数は "捕捉" されます。 捕捉の結果、変数の有効期間が延長されるため、変数は、宣言元のルーチンと共に消滅するのではなく、無名メソッド値がある限り存続します。 なお、変数の捕捉では、値ではなく変数が捕捉されることに注意してください。 変数の値が無名メソッドの生成によって捕捉された後で変更された場合、その無名メソッドが捕捉した変数の値も変更されます。両者は記憶域が共通する同じ変数だからです。 捕捉された変数は、スタックではなくヒープ上に格納されます。
無名メソッド値はメソッド参照型であり、参照カウントの対象になります。 指定された無名メソッド値への最後のメソッド参照がスコープから外れる、またはクリアされる(nil に初期化される)か終了処理されると、それによって捕捉されていた変数は最終的にスコープから出ます。
複数の無名メソッドが同じローカル変数を捕捉する場合、この状況はより複雑になります。 あらゆる状況でこれがどのように動作するかを理解するには、実装の方法をより的確に把握しておく必要があります。
ローカル変数は、捕捉されるたびに、宣言元のルーチンに関連付けられている "フレーム オブジェクト" に追加されます。 ルーチンで宣言されている無名メソッドはすべて、自らが含まれているルーチンに関連付けられているフレーム オブジェクトのメソッドに変換されます。 最後に、無名メソッド値の生成や変数の捕捉によって作成されたフレーム オブジェクトが存在し、捕捉された外部変数にアクセスする必要がある場合、そのようなフレームはどれも別の参照によって親にリンクされます。 1 つのフレーム オブジェクトからその親へのこうしたリンクもまた、参照カウントの対象になります。 ネストしたローカル ルーチン内に宣言され親ルーチンの変数を捕捉する無名メソッドは、その親フレーム オブジェクトそのものがスコープから出るまで、それを有効な状態に保ちます。
たとえば、次のような状況を考えてみましょう。
type
TProc = reference to procedure;
procedure Call(proc: TProc);
// ...
procedure Use(x: Integer);
// ...
procedure L1; // frame F1
var
v1: Integer;
procedure L2; // frame F1_1
begin
Call(procedure // frame F1_1_1
begin
Use(v1);
end);
end;
begin
Call(procedure // frame F1_2
var
v2: Integer;
begin
Use(v1);
Call(procedure // frame 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 だけの場合は、変数 v11 のみ有効な状態に保たれます。
メソッド参照/フレームのリンク チェーンに、メモリ リークの原因となる循環が生じる可能性があります。 たとえば、無名メソッドを、その無名メソッドそのものによって捕捉される変数に直接/間接的に格納すると、循環が生じ、メモリ リークの原因となります。
無名メソッドの有用性
無名メソッドがもたらすものは、呼び出し可能なものへの単なるポインタだけではありません。 その他にも、以下のようにいくつかの利点があります。
- 変数値のバインディング
- メソッドをたやすく定義および使用する方法
- コードを使ったパラメータ化が容易
変数バインディング
無名メソッドは、コード ブロックと変数バインディングを一緒に定義元の環境に提供します。その環境がスコープ外であってもそれは変わりません。 関数や手続きのポインタではそれは不可能です。
たとえば、上記のサンプル コードに含まれている adder := MakeAdder(20); というステートメントでは、値 20 への変数のバインディングをカプセル化する変数 adder ができます。
このような構文要素を実装している他のいくつかの言語では、これらをクロージャと呼んでいます。 従来、これは、adder := MakeAdder(20); のような式を評価するとクロージャができるという考え方でした。 これは、関数外で定義され関数内で参照されているすべての変数のバインディングへの参照を内部に含んだオブジェクトを表しており、そのため、それらの変数の値を捕捉することで閉じることができます。
使いやすさ
以下のサンプルでは、簡単なメソッドをいくつか定義してそれらを呼び出す典型的なクラス定義を示します。
type
TMethodPointer = procedure of object; // delegate void 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; //invoke anonymous method just defined
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 // \ code block
results[i] := ExpensiveCalculation(i); // / used as parameter
end // /
);
// use results
end;
これと、無名メソッドを使用せずに同じことを実現する方法を対比してみましょう。 無名メソッドを使用しない場合は、おそらく、仮想抽象メソッドと ExpensiveCalculation の具象下位クラスを 1 つずつ持つ "タスク" クラスを用意した後、すべてのタスクをキューに追加することになるでしょうが、その方法は自然とはとても言えず、統合されているとも言えません。
ここで、"並列 for" アルゴリズムは、コードでパラメータ化されている抽象概念になります。 これまでは、1 つ以上の抽象メソッドを持つ仮想基底クラスを使用するのが、このようなパターンを実装する一般的な方法でした。TThread クラスとその抽象メソッド Execute に注目してください。 しかし、無名メソッドを使用すると、このようなパターン(コードを用いたアルゴリズムやデータ構造のパラメータ化)ははるかに容易になります。