前言:
顾名思义,匿名方法是一个没有与之相关的名字的过程或函数。一个匿名方法将一个代码块视为一个实体,可以分配给一个变量或作为一个方法的参数使用。此外,匿名方法可以引用变量,并在定义该方法的上下文中为变量绑定值。匿名方法可以用简单的语法进行定义和使用。它们类似于其他语言中定义的闭包结构。
目录
一、语法
二、使用匿名方法
三、匿名方法的变量绑定
1. 可变的装订图示
2. 作为事件的匿名方法
3. 可变的绑定机制
三、 匿名方法的效用
1. 变量的绑定
2. 使用的便利性
3. 使用参数的代码
匿名方法的定义与普通的过程或函数类似,但没有名称。例如,下面这个函数返回一个被定义为匿名方法的函数:
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;
这个声明表示匿名方法:
一般来说,匿名函数类型被声明为过程或函数:
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;
其中(参数)是可选的。
匿名方法通常被分配给某个事情,如这些例子中:
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 包含一个匿名方法,它将值20绑定到匿名方法代码块中引用的变量y上。即使该值超出了范围,这种绑定也会持续存在。
使用方法引用的一个动机是有一个可以包含约束变量的类型,也被称为闭包值。由于闭包在其定义环境中关闭,包括在定义点引用的任何局部变量,它们有必须被释放的状态。方法引用是被管理的类型(它们是引用计数的),所以它们可以跟踪这种状态,并在必要时释放它。如果一个方法引用或闭包可以自由地分配给一个方法指针,比如一个事件,那么就很容易产生具有悬空指针或内存泄漏的错误类型的程序。
Delphi的事件是属性的一种约定。除了类型的不同,事件和属性之间没有任何区别。如果一个属性是方法指针类型的,那么它就是一个事件。
如果一个属性是方法引用类型,那么它在逻辑上也应该被视为一个事件。然而,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)或最终确定,它所捕获的变量最终也会超出范围。
在多个匿名方法捕获同一个局部变量的情况下,这种情况会更加复杂。为了理解在所有情况下这是如何工作的,有必要对实现的机制进行更精确的说明。
每当一个局部变量被捕获,它就会被添加到一个与其声明的例程相关的 "框架对象 "中。每个在例程中声明的匿名方法都会被转换成与其包含的例程相关的框架对象上的一个方法。最后,任何由于匿名方法值被构建或变量被捕获而创建的框架对象都会通过另一个引用链接到它的父框架上--如果有这样的框架存在的话,并且如果有必要的话,可以访问一个捕获的外部变量。这些从一个框架对象到它的父框架的链接也是参考计算的。在一个嵌套的本地例程中声明的匿名方法,从其父例程中捕获变量,使该父框架对象保持活力,直到它自己超出范围。
例如,考虑这种情况:
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;
每个例程和匿名方法都有一个框架标识符,以便更容易识别哪个框架对象链接到哪个:
框架F1_2_1和F1_1_1不需要框架对象,因为它们既没有声明匿名方法也没有被捕获的变量。它们也不在嵌套的匿名方法和外部捕获变量之间的任何父子关系路径上。(它们有隐含的框架存储在栈上)。
仅仅给了匿名方法F1_2_1一个引用,变量v1和v2就被保留了下来。相反,如果唯一超过F1调用时间的引用是F1_1_1,那么只有变量v1被保留下来。
有可能在方法引用/框架链接链中创建一个循环,导致内存泄漏。例如,将匿名方法直接或间接地存储在匿名方法本身捕获的变量中,就会产生一个循环,导致内存泄漏。
匿名方法提供的不仅仅是一个简单的指向可调用事物的指针。它们提供了几个优点:
匿名方法提供了一个代码块和变量绑定到它们所定义的环境中,即使该环境不在范围内。一个指向函数或过程的指针不能做到这一点。
例如,上面的代码样本中的语句adder := MakeAdder(20);产生了一个变量adder,封装了一个变量与数值20的绑定。
其他一些实现这种结构的语言把它们称为闭包。从历史上看,我们的想法是,对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.
注意到使用匿名方法的代码是多么的简单和简短。如果你想明确而简单地定义这些方法并立即使用它们,而不需要为创建一个可能永远不会在其他地方使用的类而付出开销和努力,这就是理想的做法。这样的代码更容易理解。
匿名方法使得编写以代码为参数的函数和结构更加容易,而不仅仅是数值。
多线程是匿名方法的一个很好的应用。如果你想并行地执行一些代码,你可能有一个parallel-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的具体后裔,然后将所有的任务添加到队列中--几乎没有那么自然或整合。
在这里,"并行换 "算法是被代码参数化的抽象概念。在过去,实现这种模式的常见方法是使用一个具有一个或多个抽象方法的虚拟基类;考虑TThread类和它的抽象Execute方法。然而,匿名方法使这种模式--使用代码对算法和数据结构进行参数化--变得容易得多。