RAD Studio (Common)
|
As the name suggests, an anonymous method is a procedure or function that does not have a name associated with it. An anonymous method treats a block of code as an entity that can be assigned to a variable or used as a parameter to a method. In addition, an anonymous method can refer to variables and bind values to the variables in the context in which the method is defined. Anonymous methods can be defined and used with simple syntax. They are similar to the construct of closures defined in other languages.
An anonymous method is defined similarly to a regular procedure or function, but with no name. For example, this function returns a function that is defined as an anonymous method:
function MakeAdder(y: Integer): TFuncOfInt; begin Result := { start anonymous method } function(x: Integer) begin Result := x + y; end; { end anonymous method } end;
The function MakeAdder returns a function that it declares with no name: an anonymous method.
Note that MakeAdder returns a value of type TFuncOfInt. An anonymous method type is declared as a reference to a method:
type TFuncOfInt = reference to function(x: Integer): Integer;
This declaration indicates that the anonymous method--
type TType1 = reference to procedure[(parameterlist)]; TType2 = reference to function[(parameterlist)]: returntype;
Here are a couple of examples of types:
type TSimpleProcedure = reference to procedure; TSimpleFunction = reference to function(x: string): Integer;
An anonymous method is declared as a procedure or function without a name:
// Procedure procedure[(parameters)] begin { statement block } end; // Function function[(parameters)]: returntype begin { statement block } end;
Anonymous methods are typically assigned to something, as in these examples:
myFunc := function(x: Integer): string begin Result := IntToStr(x); end; myProc := procedure(x: Integer) begin Writeln(x); end;
Anonymous methods may also be returned by functions or passed as values for parameters when calling methods. For instance, using the anonymous method variable myFunc defined just above:
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;)
Method references can also be assigned to methods as well as anonymous methods. For example:
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;
However, the converse is not true: you can't assign an anonymous method to a regular method pointer. Method references are managed types, but method pointers are unmanaged types. Thus, for type-safety reasons, assigning method references to method pointers is not supported. For instance, events are method pointer-valued properties, so you can't use an anonymous method for an event. See the section on variable binding for more information on this restriction.
A key feature of anonymous methods is that they may reference variables that are visible to them where they were defined. Furthermore, these variables can be bound to values and wrapped up with a reference to the anonymous method. This captures state and extends the lifetime of variables.
Consider again the function defined above:
function MakeAdder(y: Integer): TFuncOfInt; begin Result := function(x: Integer) begin Result := x + y; end; end;
We can create an instance of this function that binds a variable value:
var adder: TFuncOfInt; begin adder := MakeAdder(20); Writeln(adder(22)); // prints 42 end.
The variable adder contains an anonymous method that binds the value 20 to the variable y referenced in the anonymous method's code block. This binding persists even if the value goes out of scope.
A motivation for using method references is to have a type that can contain a bound variables, also known as closure values. Since closures close over their defining environment, including any local variables referenced at the point of definition, they have state that must be freed. Method references are managed types (they are reference counted), so they can keep track of this state and free it when necessary. If a method reference or closure could be freely assigned to a method pointer, such as an event, then it would be easy to create ill-typed programs with dangling pointers or memory leaks.
Delphi events are a convention for properties. There is no difference between an event and a property, except for the kind of type. If a property is of a method pointer type, then it is an event.
If a property is of a method reference type, then it should logically be considered an event too. However the IDE does not treat it as an event. This matters for classes that are installed into the IDE as components and custom controls.
Therefore, to have an event on a component or custom control that can be assigned to using a method reference or a closure value, the property must be of a method reference type. However, this is inconvenient, because the IDE does not recognize it as an event.
Here is an example of using a property with a method reference type, so it can operate as an event:
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;
To avoid creating memory leaks, it is useful to understand the variable binding process in greater detail.
Local variables defined at the start of a procedure, function or method (hereafter "routine") normally live only as long as that routine is active. Anonymous methods can extend these variables' lifetimes.
If an anonymous method refers to an outer local variable in its body, that variable is "captured". Capturing means extending the lifetime of the variable, so that it lives as long as the anonymous method value, rather than dying with its declaring routine. Note that variable capture captures variables--not values. If a variable's value changes after being captured by constructing an anonymous method, the value of the variable the anonymous method captured changes too, because they are the same variable with the same storage. Captured variables are stored on the heap, not the stack.
Anonymous method values are of the method reference type, and are reference-counted. When the last method reference to a given anonymous method value goes out of scope, or is cleared (initialized to nil) or finalized, the variables it has captured finally go out of scope.
This situation is more complicated in the case of multiple anonymous methods capturing the same local variable. To understand how this works in all situations, it is necessary to be more precise about the mechanics of the implementation.
Whenever a local variable is captured, it is added to a "frame object" associated with its declaring routine. Every anonymous method declared in a routine is converted into a method on the frame object associated with its containing routine. Finally, any frame object created because of an anonymous method value being constructed or variable being captured is chained to its parent frame by another reference--if any such frame exists and if necessary to access a captured outer variable. These links from one frame object to its parent are also reference counted. An anonymous method declared in a nested, local routine that captures variables from its parent routine keeps that parent frame object alive until it itself goes out of scope.
For example, consider this situation:
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;
Each routine and anonymous method is annotated with a frame identifier to make it easier to identify which frame object links to which:
Given only a reference to the anonymous method F1_2_1, variables v1 and v2 are kept alive. If instead, the only reference that outlives the invocation of F1 is F1_1_1, only variable v1 is kept alive.
It is possible to create a cycle in the method reference/frame link chains that causes a memory leak. For example, storing an anonymous method directly or indirectly in a variable that the anonymous method itself captures creates a cycle, causing a memory leak.
Anonymous methods offer more than just a simple pointer to something that is callable. They provide several advantages:
Anonymous methods provide a block of code along with variable bindings to the environment in which they are defined, even if that environment is not in scope. A pointer to a function or procedure can't do that.
For instance, the statement adder := MakeAdder(20); from the code sample above produces a variable adder that encapsulates the binding of a variable to the value 20.
Some other languages that implement such a construct refer to them as closures. Historically, the idea was that evaluating an expression like adder := MakeAdder(20); produced a closure. It represents an object that contains references to the bindings of all variables referenced in the function and defined outside it, thus closing it by capturing the values of the variables.
The following sample shows a typical class definition to define some simple methods and then invoke them:
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.
Contrast this to the same methods defined and invoked using anonymous methods:
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.
Notice how much simpler and shorter the code is that uses anonymous methods. This is ideal if you want to explicitly and simply define these methods and use them immediately without the overhead and effort of creating a class that may never be used anywhere else. The resulting code is easier to understand.
Anonymous methods make it easier to write functions and structures parameterized by code, not just values.
Multithreading is a good application for anonymous methods. if you want to execute some code in parallel, you might have a parallel-for function that looks like this:
type TProcOfInteger = reference to procedure(x: Integer); procedure ParallelFor(start, finish: Integer; proc: TProcOfInteger);
The ParallelFor procedure iterates a procedure over different threads. Assuming this procedure is implemented correctly and efficiently using threads or a thread pool, it could then be easily used to take advantage of multiple processors:
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;
Contrast this to how it would need to be done without anonymous methods: probably a "task" class with a virtual abstract method, with a concrete descendant for ExpensiveCalculation, and then adding all the tasks to a queue--not nearly as natural or integrated.
Here, the "parallel-for" algorithm is the abstraction that is being parameterized by code. In the past, a common way to implement this pattern is with a virtual base class with one or more abstract methods; consider the TThread class and its abstract Execute method. However, anonymous methods make this pattern--parameterizing of algorithms and data structures using code--far easier.
Copyright(C) 2009 Embarcadero Technologies, Inc. All Rights Reserved.
|
What do you think about this topic? Send feedback!
|