Delphi 2009 匿名方法

Delphi 2009中的匿名方法

 

只要delphi的精神还在,我们都会跟随。

 

正如名字所言,匿名方法是一种没有名字与之关联的过程或函数。匿名方法将一组代码当成一个整体,可赋值到变量或者作为方法的参数。另外,匿名方法在上下文中可通过指向变量或绑定变量来定义。匿名方法以简单的语法来定义和使用。这类似于其他语言的闭包定义。

语法:

匿名方法定义类似于一般的过程或函数,只是没有名字。如,该函数返回一个定义了匿名方法的函数:

function MakeAdder(y: Integer): TFuncOfInt;
begin
Result := { start anonymous method } function(x: Integer)
  begin
    Result := x + y;
    end; { end anonymous method }
end;
 

没有名字,但有参数x:integer, 这个函数返回了一个可做加法的匿名方法函数。

 .

这个函数MakeAdder返回一个函数,该函数没有名称被指定:匿名方法。

注意MakeAdder的返回类型为TFuncOfInt的值。匿名方法类型是作为参照方法来声明的。

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

这是指向function方法的一种类类型。是一个整型值?

该声明说明了匿名方法的一些特性—

。是一个函数

。有一个整型参数

。返回一个整型值

通常匿名函数类型被声明为过程或者函数:

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

匿名方法声明时并没有过程或函数名:

 

// 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)
  begin
    Result := x + y;
  end;
end;
现在来创建一个绑定了变量值的函数的实例:

var
  adder: TFuncOfInt;
begin
  adder := MakeAdder(20);//这个20是y变量的值
  Writeln(adder(22)); // prints 42,22是匿名方法的x变量值
end.
变量adder包含了一个匿名方法,在匿名方法代码块中变量y的值是20。这个绑定一直持续有效,即使变量作用域超出了范围。

作为事件的匿名方法

使用方法参照的动机就是要有这样一个类型,这个类型能包含约束变量(bound variables),正如著名的闭包值。由于闭包闭合于其定义的环境,包括在定义点的局部变量引用,它们有必须确定的状态。方法引用是被管理的类型(它们是引用计数的),所以,它们能够在必须的时候保存状态和释放。如果一个方法引用或闭包可自由地赋值给方法指针,如一个事件,那么这样就很容易创建不确定的指针和内存泄漏的病态类型程序。

Delphi事件是一些属性的约定。事件与属性本无分别,除了类型的差别之外。如果一个属性是属于方法指针类型,那么这个属性就是事件。

如果一个属性是方法引用类型,那么在逻辑上也必须认为是事件。然按,IDE并不将它当成是事件。这样的话,作为组件和定制控件的类在安装到IDE环境时,问题就来了。

因而,拥有事件的组件或定制控件能够被方法引用或闭包值来赋值,属性必须是方法引用类型。然而,这是不方便的,因为IDE不会认为它是一个事件:

type
  TProc = reference to procedure;//引用过程
  TMyComponent = class(TComponent)
  private
    FMyEvent: TProc;//引用过程变量
  public
    // MyEvent property serves as an event: 作为事件的属性MyEvent
    property MyEvent: TProc read FMyEvent write FMyEvent;//属性MyEvent:读写引用过程变量
    // 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;
每一个子程序和匿名方法都标记了框架标识,以便更易识别出是由哪个框架对象所链接的:

l         v1是F1中的变量

l         v2是F1_2(由F1_2_1捕获)中的变量

l         F1_1_1的匿名方法是 F1_1中的方法

l         F1_1链接F1(F1_1_1使用v1)

l         匿名方法F1_2是F1中的方法

l         匿名方法F1_2是F1_2中的方法

 

框架F1_2_1和F1_1_1不需要框架对象,是由于它们既不声明匿名方法也没有被捕获的变量。它们不在任何介于嵌套匿名方法和外部捕获变量的亲缘路径之上。(它们具有存储在栈中的显式的框架。)

所给出的仅仅是参照到匿名方法的F1_2_1,变量v1和v2持续生存。否则,只是超出F1引用的参照是F1_1_1,只有变量v1保持存活。

在方法引用/框架连接链中,可能出现内存泄漏的周期。例如,直接或间接存储一个匿名方法到一个变量中,然后这个匿名方法本身捕获一个创建周期,这个周期有可能造成内存泄漏。

 

匿名方法的使用(匿名方法的好处)

匿名方法提供的不仅仅是类似于可调用的指向某些东西的简单指针。它们其实拥有几个优势:

l         绑定变量值

l         定义和使用方法的捷径

l         更容易以参数化的方式来使用代码

 

变量绑定

匿名方法提供了一个代码块,变量随之绑定到由这个代码块所定义的环境当中,即使这个环境并非在一个范围当中。函数或过程的指针却不能这样做。

例如,语句 adder:=MakeAdder(20); 上面的代码样本产生了一个adder变量,这个变量封装了变量值为20的绑定。(adder绑定了具体的变量值)

一些其他语言中实现这种构造称之为闭包(closures)。从历史上看,当时的想法是在计算一个像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;
 

对比一下如果没有匿名方法的情况下是如何完成这个任务的:可能是一个带有虚抽象方法的”task”类,带有具体派生的ExpensiveCalculation,然后加上所有task到队列中 –这样既不自然也不集成。

这里,“parallel-for”算法是通过代码参数化的抽象。过去,一般方法是应用带有一个或多个抽象方法的虚拟基类来实现这种模式的;考虑一下TThread类以及他的抽象方法Execute方法。但是,匿名方法创建这种模式 – 算法的参数化和使用代码的数据结构 – 其实是更加容易了。

posted on 2008-11-26 22:39  徐龠  阅读(1896)  评论(3)    收藏  举报

导航