Anonyme Methoden in Delphi
Nach oben zu Prozeduren und Funktionen - Index
Wie der Name schon vermuten lässt, ist eine anonyme Methode eine Prozedur oder Funktion, der kein Name zugeordnet ist. Eine anonyme Methode behandelt einen Codeblock als eine Entität, die einer Variable zugewiesen oder als Parameter für eine Methode verwendet werden kann. Außerdem kann eine anonyme Methode auf Variablen verweisen und Werte an Variablen in dem Kontext, in dem die Methode definiert ist, binden. Anonyme Methoden können mit einer einfachen Syntax definiert und verwendet werden. Sie ähneln dem Konstrukt der Closures, die in anderen Sprachen definiert sind.
- Hinweis: In diesem Thema wird die Behandlung der anonymen Methoden von Delphi in Delphi-Code beschrieben. Für C++-Code, siehe Behandlung der anonymen Methoden von Delphi in C++.
Inhaltsverzeichnis
Syntax
Eine anonyme Methode wird ähnlich wie eine normale Prozedur oder Funktion, aber ohne Namen, definiert.
Beispielsweise gibt diese Funktion eine Funktion zurück, die als anonyme Methode definiert ist:
function MakeAdder(y: Integer): TFuncOfInt;
begin
Result := { start anonymous method } function(x: Integer) : Integer
begin
Result := x + y;
end; { end anonymous method }
end;
Die Funktion MakeAdder gibt eine Funktion zurück, die sie ohne Namen deklariert: eine anonyme Methode.
Bitte beachten Sie, dass MakeAdder einen Wert des Typs TFuncOfInt zurückgibt. Ein anonymer Methodentyp wird als eine Referenz auf eine Methode deklariert:
type
TFuncOfInt = reference to function(x: Integer): Integer;
Diese Deklaration gibt an, dass die anonyme Methode
- eine Funktion ist
- einen Integer-Parameter übernimmt
- einen Integerwert zurückgibt.
Im Allgemeinen wird ein anonymer Methodentyp entweder für eine Prozedur oder für eine Funktion deklariert:
type
TType1 = reference to procedure (parameterlist);
TType2 = reference to function (parameterlist): returntype;
wo (parameterlist) optional sind.
Im Folgenden finden Sie einige Beispiele für Typen:
type
TSimpleProcedure = reference to procedure;
TSimpleFunction = reference to function(x: string): Integer;
Eine anonyme Methode wird als namenlose Prozedur oder Funktion deklariert:
// Procedure
procedure (parameters)
begin
{ statement block }
end;
// Function
function (parameters): returntype
begin
{ statement block }
end;
wo (parameterlist) optional sind.
Verwendung anonymer Methoden
Anonyme Methoden werden typischerweise zu etwas zugewiesen, wie die folgenden Beispiele zeigen:
myFunc := function(x: Integer): string
begin
Result := IntToStr(x);
end;
myProc := procedure(x: Integer)
begin
Writeln(x);
end;
Anonyme Methoden können auch von Funktionen zurückgegeben oder als Werte für Parameter beim Aufruf von Methoden übergeben werden. Beispiel, in dem die oben definierte anonyme Methodenvariable myFunc verwendet wird:
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;)
Methodenreferenzen können auch zu Methoden und zu anonymen Methoden zugewiesen werden. Zum Beispiel:
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;
Das Umgekehrte trifft jedoch nicht zu: eine anonyme Methode kann keinem normalen Methodenzeiger zugewiesen werden. Methodenreferenzen sind verwaltete Typen, aber Methodenzeiger sind unverwaltete Typen. Daher wird aus Sicherheitsgründen das Zuweisen von Methodenreferenzen zu Methodenzeigern nicht unterstützt. Ereignisse sind beispielsweise Methodenzeigereigenschaften, daher können anonyme Methoden nicht für Ereignisse verwendet werden. Weitere Informationen über diese Einschränkung finden Sie in dem Abschnitt über Variablenbindung.
Variablenbindung anonymer Methoden
Ein Hauptmerkmal anonymer Methoden ist, dass sie Variablen referenzieren können, die bei der Definition der Methoden für diese sichtbar sind. Darüber hinaus können diese Variablen an Werte gebunden und mit einer Referenz auf die anonyme Methode gekapselt werden. Dadurch wird der Status von Variablen erfasst und deren Lebenszeit verlängert.
Beispiel für Variablenbindung
Auch hier wird die oben definierte Funktion verwendet:
function MakeAdder(y: Integer): TFuncOfInt;
begin
Result := function(x: Integer): Integer
begin
Result := x + y;
end;
end;
Nun kann eine Instanz dieser Funktion erstellt werden, die einen Variablenwert bindet:
var
adder: TFuncOfInt;
begin
adder := MakeAdder(20);
Writeln(adder(22)); // prints 42
end.
Die Variable adder enthält eine anonyme Methode, die den Wert 20 an die Variable y bindet, die im Codeblock der anonymen Methode referenziert ist. Diese Bindung bleibt auch dann bestehen, wenn der Wert den Gültigkeitsbereich überschreitet.
Anonyme Methoden als Ereignisse
Ein Grund für die Verwendung von Methodenreferenzen ist, dass so ein Typ vorhanden ist, der gebundene Variablen, auch Closure-Werte genannt, enthalten kann. Da Closures ihre definierende Umgebung beinhalten, einschließlich aller lokalen Variablen, die zum Zeitpunkt der Definition referenziert werden, besitzen sie einen Status, der freigegeben werden muss. Methodenreferenzen sind verwaltete Typen (die Referenzen werden gezählt), daher können sie diesen Status verfolgen und bei Bedarf freigeben. Wenn eine Methodenreferenz oder eine Closure ungehindert zu einem Methodenzeiger, wie z.B. einem Ereignis, zugewiesen werden könnte, dann wäre es leicht, fehlerhafte Programme mit verwaisten Zeigern oder Speicherlecks zu erstellen.
Delphi-Ereignisse sind eine Konvention für Eigenschaften. Es gibt keinen Unterschied zwischen einem Ereignis und einer Eigenschaft außer der Typart. Wenn eine Eigenschaft ein Methodenzeigertyp ist, dann ist sie ein Ereignis.
Wenn eine Eigenschaft ein Methodenreferenztyp ist, dann sollte sie logischerweise auch als Ereignis angesehen werden. Die IDE behandelt sich jedoch nicht als ein Ereignis. Dies ist für Klassen von Bedeutung, die in der IDE als Komponenten und benutzerdefinierte Steuerelemente installiert sind.
Daher muss für ein Ereignis einer Komponente oder eines benutzerdefinierten Steuerelements, das mit einer Methodenreferenz oder einem Closure-Wert zugewiesen werden kann, die Eigenschaft ein Methodenreferenztyp sein. Das ist jedoch unpraktisch, weil die IDE das Ereignis nicht als solches erkennt.
Hier ist ein Beispiel für die Verwendung einer Eigenschaft mit einem Methodenreferenztyp, damit sie als Ereignis verwendet werden kann:
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;
Mechanismus für das Binden von Variablen
Um das Erzeugen von Speicherlecks zu vermeiden, sollten Sie die Variablenbindung genau verstehen.
Lokale Variablen, die am Beginn einer Prozedur, Funktion oder Methode (nachstehend "Routine" genannt) definiert sind, "leben" in der Regel nur so lange, wie diese Routine aktiv ist. Anonyme Methoden können die Lebensdauer dieser Variablen verlängern.
Wenn eine anonyme Methode auf eine äußere lokale Variable in ihrem Rumpf verweist, ist die Variable "erfasst". Erfassen bedeutet, dass die Lebensdauer der Variable verlängert wird, so dass sie so lange "lebt" wie der anonyme Methodenwert und nicht mit ihrer deklarierenden Routine aufgelöst wird. Beachten Sie bitte, dass Variablenerfassung Variablen -- nicht Werte erfasst. Wenn sich der Wert einer Variablen nach dem Erfassen durch die Konstruktion einer anonymen Methode ändert, ändert sich auch der Wert der Variablen, die die anonyme Methode erfasst hat, weil es sich um dieselbe Variable mit demselben Speicher handelt. Erfasste Variablen werden auf dem Heap, nicht auf dem Stack, gespeichert.
Anonyme Methodenwerte haben den Typ Methodenreferenz und ihre Referenzen werden gezählt. Wenn die letzte Methodenreferenz auf einen anonymen Methodenwert den Gültigkeitsbereich überschreitet oder geleert wird (auf nil initialisiert wird) oder finalisiert wird, überschreiten die erfassten Variablen schließlich auch den Gültigkeitsbereich.
Diese Situation ist bei mehreren anonymen Methoden, die dieselbe lokale Variable erfassen, komplizierter. Um zu verstehen, wie dies in allen Situationen funktioniert, müssen Sie sich über die Implementierungstechnik im Klaren sein.
Immer wenn eine lokale Variable erfasst wird, wird sie einem "Frame-Objekt" hinzugefügt, das der deklarierenden Routine zugeordnet ist. Jede in einer Routine deklarierte anonyme Methode wird in eine Methode im Frame-Objekt konvertiert, das der enthaltenden Routine zugeordnet ist. Schließlich wird jedes Frame-Objekt, das erzeugt wird, weil ein anonymer Methodenwert erstellt oder eine Variable erfasst wird, mit seinem übergeordneten Frame über eine andere Referenz verkettet -- wenn ein solcher Frame vorhanden ist und es erforderlich ist, auf eine erfasste äußere Variable zuzugreifen. Diese Referenzen von einem Frame-Objekt zu seinem übergeordneten Frame werden auch gezählt. Eine anonyme Methode, die in einer verschachtelten lokalen Routine deklariert ist, die Variablen von ihrer übergeordneten Routine erfasst, hält das übergeordnete Frame-Objekt so lange am Leben, bis sie selbst den Gültigkeitsbereich überschreitet.
Sehen Sie sich das folgende Beispiel an:
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;
Jede Routine und jede anonyme Methode ist mit einem Frame-Bezeichner versehen. Dadurch kann leichter festgestellt werden, welches Frame-Objekt mit welchem anderen verknüpft ist:
- v1 ist eine Variable in F1
- v2 ist eine Variable in F1_2 (erfasst von F1_2_1)
- anonyme Methode für F1_1_1 ist eine Methode in F1_1
- F1_1 verknüpft zu F1 (F1_1_1 verwendet v1)
- anonyme Methode für F1_2 ist eine Methode in F1
- anonyme Methode für F1_2_1 ist eine Methode in F1_2
Die Frames F1_2_1 und F1_1_1 benötigen keine Frame-Objekte, weil sie weder anonyme Methoden deklarieren noch erfasste Variablen haben. Sie befinden sich auch in keinem Abstammungspfad von einer verschachtelten anonymen Methode zu einer äußeren erfassten Variable. (Sie haben implizite Frames, die auf dem Stack gespeichert sind.)
Nur durch eine Referenz zu der anonymen Methoden F1_2_1 bleiben die Variablen v1 und v2 am Leben. Wenn stattdessen die einzige Referenz, die den Aufruf von F1 überdauert, F1_1_1 ist, würde nur die Variable v1 am Leben bleiben.
Es ist möglich, in den Methodenreferenz/Frame-Verknüpfungsketten einen Zyklus zu erstellen, der ein Speicherleck verursacht. Wenn Sie beispielsweise eine anonyme Methode direkt oder indirekt in einer Variablen speichern, die von der anonymen Methode erfasst wird, wird ein Zyklus erstellt, der ein Speicherleck hervorruft.
Nutzen von anonymen Methoden
Anonyme Methoden bieten weit mehr als nur einen einfachen Zeiger auf etwas, das aufrufbar ist. Sie bieten eine Reihe von Vorteilen:
- das Binden von Variablenwerten
- eine einfache Möglichkeit zum Definieren und Verwenden von Methoden
- eine einfachen Weg zum Parametrisieren von Code
Variablenbindung
Anonyme Methoden stellen einen Codeblock zusammen mit Variablenbindungen zu der Umgebung bereit, in der sie definiert sind, auch dann, wenn diese Umgebung sich nicht im Gültigkeitsbereich befindet. Ein Zeiger oder eine Funktion oder Prozedur kann das nicht.
Beispielsweise erzeugt die Anweisung adder := MakeAdder(20); aus dem obigen Codebeispiel eine Variable adder, die die Bindung einer Variable an den Wert 20 kapselt.
In anderen Sprachen, die solche Konstrukte implementieren, werden sie Closures genannt. Die Vorstellung war, dass die Auswertung eines Ausdrucks wie adder := MakeAdder(20); eine Closure erzeugt. Sie repräsentiert ein Objekt, das Referenzen auf die Bindungen aller in der Funktion referenzierten und außerhalb definierten Variablen enthält.
Einfache Verwendung
Das folgende Beispiel zeigt eine typische Klassendefinition zur Definition und zum Aufruf einiger einfacher Methoden:
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.
Vergleichen Sie damit dieselben Methoden, die mit anonymen Methoden definiert und aufgerufen werden:
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.
Beachten Sie, um wie viel einfacher und kürzer der Code mit den anonymen Methoden ist. Dies ist ideal für die explizite und einfache Definition dieser Methoden und deren sofortiger Verwendung ohne den Mehraufwand und die Anstrengung des Erstellens einer Klasse, die möglicherweise nirgends sonst verwendet wird. Der resultierende Quelltext ist einfacher zu verstehen.
Verwendung von Code für einen Parameter
Anonyme Methoden vereinfachen das Schreiben von Funktionen und Strukturen, die durch Code, nicht nur durch Werte, parametrisiert sind.
Multithreading ist ein gutes Einsatzgebiet für anonyme Methoden. Wenn Sie Code parallel ausführen möchten, sollten Sie eine parallel-for-Funktion, wie etwa die folgende, verwenden:
type
TProcOfInteger = reference to procedure(x: Integer);
procedure ParallelFor(start, finish: Integer; proc: TProcOfInteger);
Die Prozedur ParallelFor iteriert durch eine Prozedur mit verschiedenen Threads. Angenommen, diese Prozedur ist korrekt und effizient mit Threads oder einem Thread-Pool implementiert, dann könnte sie vorteilhaft für mehrere Prozessoren eingesetzt werden:
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;
Vergleichen Sie diese Prozedur mit dem Aufwand, den Sie treiben müssten, um dasselbe ohne anonyme Methoden zu erreichen: vermutlich eine "Aufgaben"-Klasse mit einer virtuelle abstrakten Methode mit einem konkreten Nachkommen für ExpensiveCalculation erstellen und dann alle Aufgaben in eine Warteschlange stellen -- nicht annähernd so integriert.
Hier ist der "parallel-for"-Algorithmus die Abstraktion, die durch Code parametrisiert wird. Früher war ein gebräuchlicher Weg für die Implementierung dieses Beispiels die Verwendung einer virtuellen Basisklasse mit einer oder mehreren abstrakten Methoden; denken Sie an die Klasse TThread und ihre abstrakte Methode Execute. Anonyme Methoden vereinfachen dieses Beispiel -- das Parametrisieren von Algorithmen und Datenstrukturen mittels Code -- erheblich.