Méthodes anonymes dans Delphi

De RAD Studio
Aller à : navigation, rechercher

Remonter à Procédures et fonctions - Index


Comme le nom le suggère, une méthode anonyme est une procédure ou une fonction n'ayant pas de nom associé. Une méthode anonyme traite un bloc de code comme une entité qui peut être assignée à une variable ou utilisée comme paramètre d'une méthode. En outre, une méthode anonyme peut faire référence aux variables et lier des valeurs aux variables dans le contexte dans lequel la méthode a été définie. Les méthodes anonymes peuvent être définies et utilisées avec une syntaxe simple. Elles sont similaires à la construction de closures définis dans d'autres langages.

Remarque: Cette rubrique traite la gestion de la méthode anonyme Delphi dans le code Delphi. Pour le code C++, voir Comment gérer les méthodes anonymes Delphi dans C++.

Syntaxe

Une méthode anonyme est définie de la même façon qu'une procédure ou une fonction normale, mais sans nom.

Par exemple, cette fonction renvoie une fonction qui est définie comme méthode anonyme :

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

La fonction MakeAdder renvoie une fonction qu'il déclare sans nom : une méthode anonyme.

Notez que MakeAdder renvoie une valeur de type TFuncOfInt. Un type méthode anonyme est déclaré comme référence à une méthode :

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

Cette déclaration indique que la méthode anonyme :

  • est une fonction
  • prend un paramètre integer
  • renvoie une valeur integer.

En général, un type fonction anonyme est déclaré pour une procédure ou une fonction :

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

(parameterlist) est optionnel.

Voici deux exemples de types :

type
  TSimpleProcedure = reference to procedure;
  TSimpleFunction = reference to function(x: string): Integer;

Une méthode anonyme est déclarée comme une procédure ou une fonction sans nom :

// Procedure
procedure (parameters)
begin
  { statement block }
end;
// Function
function (parameters): returntype
begin
  { statement block }
end;

(parameters) est optionnel

Utilisation des méthodes anonymes

Les méthodes anonymes sont typiquement assignées à quelque chose, comme dans ces exemples :

myFunc := function(x: Integer): string
begin
  Result := IntToStr(x);
end;

myProc := procedure(x: Integer)
begin
  Writeln(x);
end;

Les méthodes anonymes peuvent également être renvoyées par des fonctions ou passées comme valeurs de paramètres lors de l'appel de méthodes. Par exemple, avec la variable de méthode anonyme myFunc définie juste devant :

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;)

Les références de méthode peuvent également être assignées aux méthodes ainsi qu'aux méthodes anonymes. Par exemple :

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;

Toutefois, la réciproque n'est pas vraie : vous ne pouvez pas assigner une méthode anonyme à un pointeur de méthode normal. Les références de méthode sont des types managés, mais les pointeurs de méthode sont des types non managés. Ainsi, pour des raisons de fiabilité des types, l'assignation de références de méthode à des pointeurs de méthode n'est pas prise en charge. Par exemple, les événements sont des propriétés de méthode évaluées en pointeur, vous ne pouvez donc pas utiliser une méthode anonyme pour un événement. Pour de plus amples informations sur cette restriction, voir la section relative à la liaison des variables.

Liaison des variables des méthodes anonymes

Une fonctionnalité clé des méthodes anonymes est qu'elles peuvent référencer des variables qui leur sont visibles là où elles ont été définies. En outre, ces variables peuvent être liées à des valeurs et encapsulées avec une référence à la méthode anonyme. Cela capture l'état et étend la durée de vie des variables.

Illustration de la liaison des variables

Considérons de nouveau la fonction définie ci-dessus :

function MakeAdder(y: Integer): TFuncOfInt;
begin
  Result := function(x: Integer): Integer
  begin
    Result := x + y;
  end;
end;

Vous pouvez créer une instance de cette fonction qui lie une valeur de variable :

var
  adder: TFuncOfInt;
begin
  adder := MakeAdder(20);
  Writeln(adder(22)); // prints 42
end.

La variable adder contient une méthode anonyme qui lie la valeur 20 à la variable y référencée dans le bloc de code de la méthode anonyme. Cette liaison persiste même si la valeur sort de la portée.

Méthodes anonymes en tant qu'événements

Une motivation pour l'utilisation des références de méthode est d'avoir un type pouvant contenir des variables liées, également connues en tant que valeurs closure. Puisque les closures se ferment sur leur environnement de définition, incluant toutes les variables locales référencées au point de définition, ils ont un état qui doit être libéré. Comme les références de méthode sont des types managés (références comptées), elles peuvent conserver la trace de cet état et le libérer quand cela est nécessaire. Si une référence de méthode ou un closure peut être assigné librement à un pointeur de méthode, tel qu'un événement, il est alors facile de créer des programmes mal formés avec des pointeurs incorrects ou des pertes de mémoire.

Les événements Delphi sont une convention pour les propriétés. Il n'a pas de différence entre un événement et une propriété, à l'exception du type. Si une propriété est de type pointeur de méthode, c'est alors un événement.

Si une propriété est de type référence de méthode, elle devrait être aussi logiquement considérée comme un événement. Toutefois, l'EDI ne la traite pas comme un événement. C'est important pour les classes qui sont installées dans l'EDI, comme les composants et les contrôles personnalisés.

Par conséquent, pour avoir un événement sur un composant ou un contrôle personnalisé qui peut être assigné à l'aide d'une référence de méthode ou d'une valeur closure, la propriété doit être de type référence de méthode. Toutefois, c'est peu pratique car l'EDI ne la reconnaît pas comme un événement.

Voici un exemple d'utilisation d'une propriété avec un type référence de méthode, afin qu'elle puisse opérer comme un événement :

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;

Mécanisme de liaison des variables

Pour éviter la création de pertes de mémoire, il est utile de bien comprendre le processus de liaison des variables.

Les variables locales définies au début d'une procédure, fonction ou méthode (ci-après une "routine") vivent normalement tant que la routine est active. Les méthodes anonymes peuvent étendre la durée de vie de ces variables.

Si une méthode anonyme fait référence à une variable locale externe dans son corps, cette variable est "capturée". La capture signifie l'extension de la durée de vie de la variable, afin qu'elle puisse vivre aussi longtemps que la valeur de la méthode anonyme, plutôt que de mourir avec sa routine de déclaration. Notez que cette capture de variable capture les variables, et pas les valeurs. Si la valeur d'une variable change après sa capture par construction d'une méthode anonyme, la valeur de la variable que la méthode anonyme a capturée change aussi, car il s'agit de la même variable avec le même stockage. Les variables capturées sont stockées sur le tas, pas dans la pile.

Les valeurs de la méthode anonyme sont de type référence de méthode, et les références sont comptées. Quand la dernière référence de méthode à une valeur de méthode anonyme donnée sort de la portée, est effacée (initialisée à nil) ou finalisée, les variables capturées sortent de la portée.

Cette situation est plus compliquée dans le cas où plusieurs méthodes anonymes capturent la même variable locale. Pour comprendre comment cela fonctionne dans toutes les situations, il est nécessaire d'être plus précis sur le processus d'implémentation.

A chaque fois qu'une variable locale est capturée, elle est ajoutée à un "objet cadre" associé à sa routine de déclaration. Chaque méthode anonyme déclarée dans une routine est convertie en une méthode de l'objet cadre associé à sa routine conteneur. Enfin, tout objet cadre créé à la suite de la construction d'une valeur de méthode anonyme ou de la capture d'une variable est chaîné à son cadre parent par une autre référence, si un tel cadre existe et s'il est nécessaire d'accéder à une variable externe capturée. Ces liens d'un objet cadre à son cadre parent sont également comptabilisés par références. Une méthode anonyme déclarée dans une routine locale imbriquée qui capture les variables de sa routine parent conserve cet objet cadre parent en vie tant qu'elle ne sort pas elle-même de la portée.

Considérons, par exemple, la situation suivante :

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;

Chaque routine et méthode anonyme est annotée d'un identificateur de cadre permettant de faciliter l'identification des liens d'objets cadre :

  • v1 est une variable dans F1
  • v2 est une variable dans F1_2 (capturé par F1_2_1)
  • la méthode anonyme pour F1_1_1 est une méthode dans F1_1
  • F1_1 est lié à F1 (F1_1_1 utilise v1)
  • la méthode anonyme pour F1_2 est une méthode dans F1
  • la méthode anonyme pour F1_2_1 est une méthode dans F1_2

Les cadres F1_2_1 et F1_1_1 ne requièrent pas d'objets cadre, puisqu'ils ne déclarent pas de méthodes anonymes et qu'ils ne contiennent pas de variables capturées. Ils ne sont pas sur un chemin de parentage entre une méthode anonyme incorporée et une variable capturée externe. Ils ont des cadres implicites stockés sur la pile.

Avec seulement une référence à la méthode anonyme F1_2_1, les variables v1 et v2 sont gardées en vie. Si à la place, la seule référence qui survit à l'invocation de F1 est F1_1_1, seule la variable v1 est gardée en vie.

Il est possible de créer un cycle dans les chaînes de liens cadre/référence de méthode qui provoque une perte de mémoire. Par exemple, le stockage d'une méthode anonyme directement ou indirectement dans une variable, que la méthode anonyme elle-même capture, crée un cycle, ce qui provoque une perte de mémoire.

Utilité des méthodes anonymes

Les méthodes anonymes offrent plus qu'un simple pointeur sur quelque chose qui peut être appelé. Elles offrent plusieurs avantages :

  • la liaison des valeurs des variables
  • un moyen facile de définir et d'utiliser les méthodes
  • un paramétrage facile à l'aide du code

Liaison des variables

Les méthodes anonymes fournissent un bloc de code avec des liaisons de variables à l'environnement dans lequel elles sont définies, même si cet environnement n'est pas dans la portée. Un pointeur sur une fonction ou une procédure ne peut pas faire cela.

Par exemple, l'instruction adder := MakeAdder(20); de l'exemple de code ci-dessus produit une variable adder qui encapsule la liaison d'une variable à la valeur 20.

Certains autres langages qui implémentent une telle construction y font référence en tant que closures. Historiquement, l'idée était que l'évaluation d'une expression comme adder := MakeAdder(20);produisait un closure. Elle représente un objet qui contient des références aux liaisons de toutes les variables référencées dans la fonction et définies en dehors, le fermant ainsi en capturant les valeurs des variables.

Utilisation facile

L'exemple suivant montre une définition de classe typique permettant de définir quelques méthodes simples, puis de les invoquer :

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.

Contrastez cela avec les mêmes méthodes définies et invoquées à l'aide des méthodes anonymes :

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.

Notez que le code qui utilise les méthodes anonymes est plus simple et plus court. Cela est idéal si vous voulez définir explicitement et simplement ces méthodes et les utiliser immédiatement sans effort supplémentaire de création d'une classe qui pourrait ne jamais servir ailleurs. Le code résultant est plus facile à comprendre.

Utilisation du code pour un paramètre

Les méthodes anonymes rendent plus faciles l'écriture des fonctions et des structures paramétrées par code (pas seulement les valeurs).

La gestion multithread est une bonne application pour les méthodes anonymes. Si vous voulez exécuter du code en parallèle, vous pourriez avoir une fonction ParallelFor ressemblant à ceci :

type
  TProcOfInteger = reference to procedure(x: Integer);

procedure ParallelFor(start, finish: Integer; proc: TProcOfInteger);

La procédure ParallelFor passe en revue les différents threads d'une procédure. En supposant que cette procédure est implémentée correctement et de manière efficace à l'aide de threads ou d'un pool de threads, elle pourrait alors être facilement utilisée pour tirer profit des processeurs multiples :

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;

Contrastez cela avec la façon de faire sans les méthodes anonymes : probablement une classe "tâche" avec une méthode abstraite virtuelle, un descendant concret pour ExpensiveCalculation, puis en ajoutant toutes les tâches à une file d'attente.

Ici, l'algorithme "parallel-for" est l'abstraction qui est paramétrée par code. Auparavant, un moyen commun d'implémenter ce pattern incluait une classe de base virtuelle avec une ou plusieurs méthodes abstraites ; considérez la classe TThread et sa méthode abstraite Execute. En revanche, les méthodes anonymes rendent ce pattern, paramétrage des algorithmes et des structures de données, plus facile.

Voir aussi