Anonyme Methoden in Object Pascal

Aus Appmethod Topics
Wechseln zu: Navigation, Suche

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.

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 der anonymen Methode } function(x: Integer) : Integer
  begin
    Result := x + y;
    end; { Ende der anonymen Methode }
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;

(parameterlist) sind optional.

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:


// Prozedur
procedure[(parameters)]
begin
  { Anweisungsblock }
end;
// Funktion
function[(parameters)]: returntype
begin
  { Anweisungsblock }
end;

(parameters) sind optional.

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
  { Quelltext }
end;

// Prozedur mit anonymer Methode als Parameter aufrufen
// Verwenden der Variable:
AnalyzeFunction(myFunc);

// Anonyme Methode direkt verwenden:
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;   //Zuweisung zu einer Methodenreferenz
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)); // Gibt 42 aus
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.

Object Pascal-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
    // Eigenschaft MyEvent dient als Ereignis:
    property MyEvent: TProc read FMyEvent write FMyEvent;
    // anderer Quelltext ruft FMyEvent als gewöhnliches Beispiel für Ereignisse auf
  end;

...

var
  c : TMyComponent;
begin
  c := TMyComponent.Create(Self);
  c.MyEvent := procedure
  begin
    ShowMessage('Hello World!'); // wird angezeigt, wenn TMyComponent MyEvent aufruft
  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; // Delegat 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;   //Aufruf der gerade definierten anonymen Methode

  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                                           //  \ Codeblock
      results[i] := ExpensiveCalculation(i);        //  /  als Parameter verwendet
    end                                             // /
    );
  // Ergebnisse verwenden
  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.

Siehe auch