Difference between revisions of "for-in loop/fr"

From Free Pascal wiki
Jump to navigationJump to search
m (Fixed syntax highlighting)
 
(15 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
{{for-in loop}}
 
{{for-in loop}}
  
La construction de boucle "for-in" est pris en charge dans Delphi de Delphi 2005. Il a été mis en œuvre en FPC 2.4.2.
+
La construction de boucle "for-in" est prise en charge dans Delphi depuis Delphi 2005. Elle a été mise en œuvre dans FPC 2.4.2.
  
== Implémentation Delphi et FPC ==
+
= Implémentation Delphi et FPC =
  
 
Une boucle '''for in''' respecte la syntaxe suivante:
 
Une boucle '''for in''' respecte la syntaxe suivante:
  
=== Boucle sur une chaîne ===
+
== Boucle sur une chaîne ==
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
procedure StringLoop(S: String);
 
procedure StringLoop(S: String);
 
var
 
var
Line 19: Line 19:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Boucle sur un tableau ===
+
== Boucle sur un tableau ==
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
procedure ArrayLoop(A: Array of Byte);
 
procedure ArrayLoop(A: Array of Byte);
 
var
 
var
Line 31: Line 31:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Boucle sur un ensemble  ===
+
== Boucle sur un ensemble  ==
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
type
 
type
 
   TColor = (cRed, cGren, cBlue);
 
   TColor = (cRed, cGren, cBlue);
Line 46: Line 46:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Traverser un conteneur ===
+
== Traverser un conteneur ==
  
 
Pour travrser une classe conteneur, vous avez besoin d'ajouter un '''énumérateur'''. Un '''Enumérateur''' est une classe construite selon le modèle suivant :
 
Pour travrser une classe conteneur, vous avez besoin d'ajouter un '''énumérateur'''. Un '''Enumérateur''' est une classe construite selon le modèle suivant :
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
TSomeEnumerator = class
 
TSomeEnumerator = class
 
public
 
public
Line 58: Line 58:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Il faut seulement 2 choses pour définir une classe Enumerateur: une méthode '''MoveNext''' qui demande à l'énumérateur d'avancer d'un pas et une propriété '''Current''' qui peut retourner tout type approprié.
+
Il faut seulement deux choses pour définir une classe Enumerateur: une méthode '''MoveNext''' qui demande à l'énumérateur d'avancer d'un pas et une propriété '''Current''' qui peut retourner tout type approprié.
  
 
Par la suite, vous devez ajouter la méthode magique '''GetEnumerator''' de la classe conteneur qui retourne un '''énumérateur''' de l'instance.
 
Par la suite, vous devez ajouter la méthode magique '''GetEnumerator''' de la classe conteneur qui retourne un '''énumérateur''' de l'instance.
  
 
Par exemple:
 
Par exemple:
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
type
 
type
 
   TEnumerableTree = class;
 
   TEnumerableTree = class;
Line 91: Line 91:
 
function TTreeEnumerator.MoveNext: Boolean;
 
function TTreeEnumerator.MoveNext: Boolean;
 
begin
 
begin
   // pour obtenir le noeud suivant dans l'arbre
+
   // pour obtenir le nœud suivant dans l'arbre
 
   if FCurrent = nil then
 
   if FCurrent = nil then
 
     FCurrent := FTree.GetFirstNode
 
     FCurrent := FTree.GetFirstNode
Line 109: Line 109:
 
Ensuite, cela vous permet de d'exécuter le code suivant:
 
Ensuite, cela vous permet de d'exécuter le code suivant:
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
procedure TreeLoop(ATree: TEnumerableTree);
 
procedure TreeLoop(ATree: TEnumerableTree);
 
var
 
var
Line 122: Line 122:
  
 
Il est aussi possible de rendre une classe énumérable si vous implémentez l'interface suivante dans votre classe conteneur enumérable:
 
Il est aussi possible de rendre une classe énumérable si vous implémentez l'interface suivante dans votre classe conteneur enumérable:
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
   IEnumerable = interface(IInterface)
 
   IEnumerable = interface(IInterface)
 
     function GetEnumerator: IEnumerator;
 
     function GetEnumerator: IEnumerator;
Line 128: Line 128:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
ooù <b>IEnumerator</b> est déclarée comme :
+
<b>IEnumerator</b> est déclarée comme :
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
   IEnumerator = interface(IInterface)
 
   IEnumerator = interface(IInterface)
 
     function GetCurrent: TObject;
 
     function GetCurrent: TObject;
Line 138: Line 138:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Enumérateurs multiple pour une classe ===
+
== Enumérateurs multiples pour une classe ==
  
Vous pouvez ajouter des énumérateur supplémentaires à des classes, des objets et des enregistrements.
+
Vous pouvez ajouter des énumérateurs supplémentaires à des classes, des objets et des enregistrements.
  
 
Voici un exemple d'ajout d'énumérateur qui traverse un TEnumerableTree en ordre inverse:
 
Voici un exemple d'ajout d'énumérateur qui traverse un TEnumerableTree en ordre inverse:
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
type
 
type
 
   TEnumerableTree = class;
 
   TEnumerableTree = class;
  
 
   TTreeEnumerator = class
 
   TTreeEnumerator = class
   ...pour la traversée dans l'ordre, voir au dessus...
+
   // ...pour la traversée dans l'ordre, voir ci-dessus...
 
   end;
 
   end;
  
Line 169: Line 169:
 
   end;
 
   end;
  
...voir au dessus l'implémentation de TTreeEnumerator...
+
//...voir ci-dessus l'implémentation de TTreeEnumerator...
  
 
constructor TTreeReverseEnumerator.Create(ATree: TEnumerableTree);
 
constructor TTreeReverseEnumerator.Create(ATree: TEnumerableTree);
Line 199: Line 199:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Après, vous êtes en mesure d'exécuter le code suivant:  
+
Ensuite vous êtes en mesure d'exécuter le code suivant:  
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
procedure TreeLoop(ATree: TEnumerableTree);
 
procedure TreeLoop(ATree: TEnumerableTree);
 
var
 
var
Line 211: Line 211:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== FPC extensions ==
+
= Extensions FPC =
The following code examples illustrate constructs that are implemented only by FPC, constructs which are not supported by Delphi.
+
Les exemples de code suivants illustrent les constructions implémentées uniquement dans FPC ; elles ne sont pas supportées par Delphi.
  
=== Traversing enumeration and subrange types ===
+
== Traversée de types énumération et intervalle ==
  
In Delphi, it is [http://qc.embarcadero.com/wc/qcmain.aspx?d=106212 not possible to traverse either enumerated types] or subrange types, whereas in Free Pascal we can write the following:
+
Dans Delphi, il n'est [http://qc.embarcadero.com/wc/qcmain.aspx?d=106212 pas possible de traverser non plus les types énumérés] ou les intervalles, alors qu'en Free Pascal vous pouvez écrire ce qui suit:
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
type
 
type
 
   TColor = (clRed, clBlue, clBlack);
 
   TColor = (clRed, clBlue, clBlack);
Line 233: Line 233:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Declaring enumerators ===
+
== Déclarer des énumérateurs ==
  
It is also not possible in Delphi to add an enumerator without modifying the class, nor to add an enumerator to the non-class/object/record/interface type.
+
Il n'est pas non plus possible dans Delphi d'ajouter un énumérateur sans modifier la classe, ni d'ajouter un énumérateur d'un type non class object/record/interface. FPC rend cela possible en utilisant la nouvelle syntaxe '''operator''' type '''Enumerator'''. Comme dans l'exemple suivant:
FPC makes this possible using the new syntax <b>operator</b> type <b>Enumerator</b>. As in the following example:
 
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
type
 
type
 
   TMyRecord = record F1: Integer; F2: array of TMyType; end;
 
   TMyRecord = record F1: Integer; F2: array of TMyType; end;
Line 250: Line 249:
 
   end;
 
   end;
  
   // This is new built-in operator.
+
   // C'est le nouveau opérateur intégré.
 
   operator Enumerator(const A: TMyRecord): TMyArrayEnumerator;
 
   operator Enumerator(const A: TMyRecord): TMyArrayEnumerator;
 
   begin
 
   begin
Line 265: Line 264:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
==== Traversing UTF-8 strings ====
+
===Traversée de chaînes UTF-8===
As a particularly useful example, the above extension allows
+
Comme exemple particulièrement utile, l'extension ci-dessous autorise une traversée très efficace des chaînes UTF-8:
very efficient traversal of UTF-8 strings:
 
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
uses
 
uses
 
   LazUTF8;
 
   LazUTF8;
 
interface
 
interface
 
type
 
type
   { TUTF8StringAsStringEnumerator
+
   { TUTF8StringAsStringEnumerator  
     Traversing UTF8 codepoints as strings is useful when you want to know the
+
     La traversée de 'codepoint' UTF8 est utile quand vous voulez connaître l'encodage exact d'un
    exact encoding of the UTF8 character or if you like to use string
+
     caractère UTF-8 ou si vous aimez utiliser des constante de chaîne dans votre code.
     constants in your code.
+
     Pour des raisons de sécurité, vous pourriez utiliser les valeurs des points de code (cardinaux) à la place.
     For security reasons you should use the codepoints values (cardinals) instead.
+
     Si la vitesse importe, n'utilisez pas des énumérateurs. Employez à la place directement le PChar
     If speed matters, don't use enumerators. Instead use the PChar directly as
+
     comme montré dans la méthode MoveNext et lisez sur l'UTF8. Il a des caractéristiques intéressantes. }
     shown in the MoveNext method and read about UTF8. It has some interesting features. }
 
  
 
   TUTF8StringAsStringEnumerator = class
 
   TUTF8StringAsStringEnumerator = class
Line 314: Line 311:
 
constructor TUTF8StringAsStringEnumerator.Create(const A: UTF8String);
 
constructor TUTF8StringAsStringEnumerator.Create(const A: UTF8String);
 
begin
 
begin
   fCurrentPos:=PChar(A); // Note: if A='' then PChar(A) returns a pointer to a #0 string
+
   fCurrentPos:=PChar(A); // Note: si A='' alors PChar(A) retourne un pointeur sur une chaîne #0
 
   fEndPos:=fCurrentPos+length(A);
 
   fEndPos:=fCurrentPos+length(A);
 
end;
 
end;
Line 342: Line 339:
 
   s:='mäßig';
 
   s:='mäßig';
  
   // using UTF8Length and UTF8Copy this way is slow, requiring O(n)^2
+
   // Utiliser UTF8Length et UTF8Copy de cette manière est très lent, nécessitant O(n)^2
 
   for i:=1 to UTF8Length(s) do
 
   for i:=1 to UTF8Length(s) do
 
     writeln('ch=',UTF8Copy(s,i,1));
 
     writeln('ch=',UTF8Copy(s,i,1));
  
   // using the above enumerator is shorter and quite fast, requiring O(n)
+
   // L'emploi de l'énumérateur du dessus est plus court et plutôt rapide, nécessitant O(n)
 
   for ch in s do
 
   for ch in s do
 
     writeln('ch=',ch);
 
     writeln('ch=',ch);
Line 352: Line 349:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Using any identifiers instead of builtin MoveNext and Current ===
+
== Utiliser des identificateurs au lieu de MoveNext et Current ==
In Delphi you must use a function with the name 'MoveNext' and a property with the name 'Current' in enumerators. With FPC you can choose whatever names you wish. This is enabled by the use of the '''enumerator''' modifier, with the syntax 'enumerator MoveNext;' and 'enumerator Current;' modifiers. As in the following example:
+
En Delphi, vous devez utiliser une fonction avec le nom 'MoveNext' et une propriété avec le nom 'Current' dans les énumérateurs. Avec FPC, vous pouvez choisir n'importe quel nom. Cela est permis par l'emploi du modifieur '''enumerator''', avec la syntaxe des modifieurs 'enumerator MoveNext;' et 'enumerator Current;'. Comme dans l'exemple suivant :
  
<syntaxhighlight>
+
<syntaxhighlight lang=pascal>
 
type
 
type
 
   { TMyListEnumerator }
 
   { TMyListEnumerator }
Line 406: Line 403:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== Proposed extensions ==
+
= Extensions proposées =
  
 +
== Donner la position de l'énumérateur si disponible ==
 +
===Problématique===
 +
Il est impossible d'extraire toute information de l'itérateur sauf l'article courant. Parfois d'autres informations, comme l'index courant, pourraient être utiles :
  
=== Get enumerator Position if available ===
+
<syntaxhighlight lang=pascal>
It is impossible to extract any information from the iterator
 
except the current item. Sometimes other data, such as the current index, might be useful:
 
 
 
<syntaxhighlight>
 
 
type
 
type
 
   TUTF8StringEnumerator = class
 
   TUTF8StringEnumerator = class
Line 437: Line 433:
 
begin
 
begin
  
   // Inefficient, as discussed above
+
   // inefficace comme discuté plus haut
 
   for i := 1 to Length(s) do
 
   for i := 1 to Length(s) do
 
     Writeln(i, ': ', ch[i]);
 
     Writeln(i, ': ', ch[i]);
  
   // Ok, but ugly
+
   // Ok, mais moche
 
   i := 1;
 
   i := 1;
 
   for ch in s do begin
 
   for ch in s do begin
Line 448: Line 444:
 
   end;
 
   end;
  
   // Proposed extension
+
   // Extension proposée
 
   for ch in s index i do
 
   for ch in s index i do
 
     Writeln(i, ': ', ch);
 
     Writeln(i, ': ', ch);
  
   // Proposed extension for traversing backwards (equivalent to downto)
+
   // Extension proposée pour traverser à l'envers (équivalent au downto)
 
   for ch in reverse s do
 
   for ch in reverse s do
 
     Writeln(i, ': ', ch);
 
     Writeln(i, ': ', ch);
  
   // With proposed index extension
+
   // Avec l'extension d'index proposée
 
   for ch in reverse s index i do
 
   for ch in reverse s index i do
 
     Writeln(i, ': ', ch);
 
     Writeln(i, ': ', ch);
Line 462: Line 458:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Note that index could be designed to return an arbitrary type (i. e. not necessarily an integer).
+
Remarquez que l'index doit être conçu pour retourner un type arbitraire  (c'est-à-dire pas forcément un entier).
For example, in the case of tree traversal, the index might return an array of
+
Par exemple, dans l'exemple d'une traversée d'arbre, l'index pourrait retourner un tableau de nœuds
nodes describing the path from the tree root to the current node.
+
décrivant le chemin allant de la racine au nœud courant.
 +
 
 +
===Solution de contournement pour l'indice===
 +
La propriété Current prend la forme d'un couple (Indice,Valeur), on emploie un type générique afin de pérenniser cette conception.
 +
 
 +
<syntaxhighlight lang=pascal>
 +
unit IndicedEnumerators;
 +
 
 +
interface
 +
 
 +
type
 +
 
 +
generic TGIndicedEnumerator<TContainer, TElement> = class
 +
public type
 +
  TIndicedValue = record
 +
    Value: TElement;
 +
    Ind: Integer;
 +
  end;
 +
protected
 +
  FCntnr: TContainer;
 +
  FCurrent: TIndicedValue;
 +
public
 +
  constructor Create(aCntnr: TContainer); virtual;
 +
  function MoveNext: Boolean; virtual; abstract;
 +
  property Current: TIndicedValue read FCurrent;
 +
  function GetEnumerator: TGIndicedEnumerator;
 +
end;
  
== Reference ==
+
TCsvAnsiStringFieldEnumerator = class(specialize TGIndicedEnumerator<AnsiString, AnsiString>)
 +
private
 +
  FCurrPos: Integer;
 +
  FSeparator: Char;
 +
public
 +
  constructor Create(aString: AnsiString; Separator: AnsiChar); overload;
 +
  function MoveNext: Boolean; override;
 +
end;
 +
 
 +
implementation
 +
 
 +
{ TGIndicedEnumerator }
 +
 
 +
constructor TGIndicedEnumerator.Create(aCntnr: TContainer);
 +
begin
 +
  FCntnr := aCntnr;
 +
end;
 +
 
 +
function TGIndicedEnumerator.GetEnumerator: TGIndicedEnumerator;
 +
begin
 +
  Result := Self;
 +
end;
 +
 
 +
{ TCsvAnsiStringFieldEnumerator }
 +
 
 +
constructor TCsvAnsiStringFieldEnumerator.Create(aString: AnsiString; Separator: AnsiChar);
 +
begin
 +
  inherited Create(aString);
 +
  FCurrPos := 0;
 +
  FSeparator := Separator;
 +
  FCurrent.Ind := -1;
 +
end;
 +
 
 +
function TCsvAnsiStringFieldEnumerator.MoveNext: Boolean;
 +
begin
 +
  if FCurrPos > Length(FCntnr) then exit(False);
 +
  // Premier élément
 +
  Inc(FCurrent.ind);
 +
  FCurrent.Value := '';
 +
  Inc(FCurrPos);
 +
  while (FCurrPos <= Length(FCntnr)) and (FCntnr[FCurrPos] <> FSeparator) do
 +
  begin
 +
    FCurrent.Value += FCntnr[FCurrPos];
 +
    Inc(FCurrPos);
 +
  end;
 +
  Result := True;
 +
end;
 +
 
 +
end.
 +
</syntaxhighlight>
 +
 
 +
Un point intéressant se trouve dans la déclaration de GetEnumerator dont le type de retour est le type énumérateur lui-même, nécessaire pour pouvoir créer un itérateur à partir de la classe elle-même, mais surtout la méthode sera correctement adaptée lors de la spécialisation et retournera alors le type spécialisé (TCsvAnsiStringFieldEnumerator dans notre exemple). Ce qui montre que la substitution lors de spécialisation ne se limite aux seules marques substitutives ("placeholders").
 +
 
 +
Dans l'exemple, la spécialisation et la dérivation sont réalisées simultanément mais ce n'est pas une nécessité, il est d'ailleurs sans doute préférable de scinder ces deux opérations et de créer un type intermédiaire spécialisé TAnsiStringEnumerator qui sans introduire la notion de séparateur permettrait de concevoir d'autres itérateurs sur des AnsiString.
 +
 
 +
L'enregistrement couple (Indice,Valeur) lui-même générique sera automatiquement adapté lors de la spécialisation, cependant, son nom de type devra être qualifié à l'aide du nom de la classe d'énumérateur spécialisé comme nous pouvons le voir dans le petit programme d'exemple, afin de lever toute ambiguïté si deux types spécialisés devaient cohabiter :
 +
<syntaxhighlight lang=pascal>
 +
#!/usr/bin/instantfpc
 +
 
 +
// (see the Instantfpc doc)
 +
 
 +
{$mode objfpc}{$H+}
 +
 
 +
uses
 +
  SysUtils,
 +
  IndicedEnumerators; 
 +
 
 +
var
 +
  IndStr: TCsvAnsiStringFieldEnumerator.TIndicedValue;
 +
 
 +
begin
 +
  // énumérateur obtenu directement à partir de la classe elle-même, la libération de l'énumérateur
 +
  // est garantie en sortie de la boucle for .. in
 +
  for IndStr in TCsvAnsiStringFieldEnumerator.Create(ParamStr(1), ';') do
 +
    WriteLn(Format('%d : "%s"', [IndStr.Ind, IndStr.Value]));
 +
end.
 +
</syntaxhighlight>
 +
 
 +
Appel du script InstantFPC en ligne de commande (nom du fichier source pascal avec des droit d'exécution sous Linux + un paramètre)
 +
 
 +
  ./TestStringFieldEnums.pas "small;string;with;separated;fields"
 +
 
 +
Cette solution a l'avantage de mettre à disposition un indice même si celui-ci n'existe pas nativement dans le type traversé, sans avoir à déclarer une variable Indice dans le code client.
 +
 
 +
Enfin, l'énumérateur générique pourrait être amélioré en ajoutant aux paramètres du constructeur un indice de départ (0 devenant la valeur par défaut).
 +
 
 +
= Référence =
 
* http://17slon.com/blogs/gabr/2007/03/fun-with-enumerators.html
 
* http://17slon.com/blogs/gabr/2007/03/fun-with-enumerators.html
 
[[Category:FPC]]
 
[[Category:Pascal]]
 
[[Category:Control Structures]]
 

Latest revision as of 01:20, 16 February 2020

English (en) français (fr) 日本語 (ja) русский (ru)

La construction de boucle "for-in" est prise en charge dans Delphi depuis Delphi 2005. Elle a été mise en œuvre dans FPC 2.4.2.

Implémentation Delphi et FPC

Une boucle for in respecte la syntaxe suivante:

Boucle sur une chaîne

procedure StringLoop(S: String);
var
  C: Char;
begin
  for C in S do
    DoSomething(C);
end;

Boucle sur un tableau

procedure ArrayLoop(A: Array of Byte);
var
  B: Byte;
begin
  for B in A do
    DoSomething(B);
end;

Boucle sur un ensemble

type
  TColor = (cRed, cGren, cBlue);
  TColors = set of TColor;
procedure SetLoop(Colors: TColors);
var
  Color: TColor;
begin
  for Color in Colors do
    DoSomething(Color);
end;

Traverser un conteneur

Pour travrser une classe conteneur, vous avez besoin d'ajouter un énumérateur. Un Enumérateur est une classe construite selon le modèle suivant :

TSomeEnumerator = class
public
  function MoveNext: Boolean;
  property Current: TSomeType;
end;

Il faut seulement deux choses pour définir une classe Enumerateur: une méthode MoveNext qui demande à l'énumérateur d'avancer d'un pas et une propriété Current qui peut retourner tout type approprié.

Par la suite, vous devez ajouter la méthode magique GetEnumerator de la classe conteneur qui retourne un énumérateur de l'instance.

Par exemple:

type
  TEnumerableTree = class;

  TTreeEnumerator = class
  private
    FTree: TEnumerableTree;
    FCurrent: TNode;
  public
    constructor Create(ATree: TEnumerableTree); 
    function MoveNext: Boolean;
    property Current: TNode read FCurrent;
  end;

  TEnumerableTree = class
  public
    function GetEnumerator: TTreeEnumerator;
  end;

constructor TTreeEnumerator.Create(ATree: TEnumerableTree);
begin
  inherited Create;
  FTree := ATree;
  FCurrent := nil;
end;

function TTreeEnumerator.MoveNext: Boolean;
begin
  // pour obtenir le nœud suivant dans l'arbre
  if FCurrent = nil then
    FCurrent := FTree.GetFirstNode
  else
    FCurrent := FTree.GetNextNode(FCurrent);
  Result := FCurrent <> nil;
end;

function TEnumerableTree.GetEnumerator: TTreeEnumerator;
begin
  Result := TTreeEnumerator.Create(Self);
  // Important: l'objet créé est automatiquement libéré par le compilateur après la boucle.
end;

Ensuite, cela vous permet de d'exécuter le code suivant:

procedure TreeLoop(ATree: TEnumerableTree);
var
  ANode: TNode;
begin
  for ANode in ATree do
    DoSomething(ANode);
end;

Vos trouverez que plusieurs classes de bases (telles que TList, TStrings, TCollection, TComponent ...) ont des énumérateurs intégrés.

Il est aussi possible de rendre une classe énumérable si vous implémentez l'interface suivante dans votre classe conteneur enumérable:

  IEnumerable = interface(IInterface)
    function GetEnumerator: IEnumerator;
  end;

IEnumerator est déclarée comme :

  IEnumerator = interface(IInterface)
    function GetCurrent: TObject;
    function MoveNext: Boolean;
    procedure Reset;
    property Current: TObject read GetCurrent;
  end;

Enumérateurs multiples pour une classe

Vous pouvez ajouter des énumérateurs supplémentaires à des classes, des objets et des enregistrements.

Voici un exemple d'ajout d'énumérateur qui traverse un TEnumerableTree en ordre inverse:

type
  TEnumerableTree = class;

  TTreeEnumerator = class
  // ...pour la traversée dans l'ordre, voir ci-dessus...
  end;

  TTreeReverseEnumerator = class
  private
    FTree: TEnumerableTree;
    FCurrent: TNode;
  public
    constructor Create(ATree: TEnumerableTree); 
    function MoveNext: Boolean;
    property Current: TNode read FCurrent;
    function GetEnumerator: TTreeReverseEnumerator; // se retourne lui-même
  end;

  TEnumerableTree = class
  public
    function GetEnumerator: TTreeEnumerator;
    function GetReverseEnumerator: TTreeReverseEnumerator;
  end;

//...voir ci-dessus l'implémentation de TTreeEnumerator...

constructor TTreeReverseEnumerator.Create(ATree: TEnumerableTree);
begin
  inherited Create;
  FTree := ATree;
end;

function TTreeReverseEnumerator.MoveNext: Boolean;
begin
  // some logic to get the next node from a tree in reverse order
  if FCurrent = nil then
    FCurrent := FTree.GetLastNode
  else
    FCurrent := FTree.GetPrevNode(FCurrent);
  Result := FCurrent <> nil;
end;

function TTreeReverseEnumerator.GetEnumerator: TTreeReverseEnumerator;
begin
  Result := Self;
end;

function TEnumerableTree.GetReverseEnumerator: TTreeReverseEnumerator;
begin
  Result := TTreeReverseEnumerator.Create(Self);
  // Note: l'objet est automatiquement supprimé par le compilateur après la boucle.
end;

Ensuite vous êtes en mesure d'exécuter le code suivant:

procedure TreeLoop(ATree: TEnumerableTree);
var
  ANode: TNode;
begin
  for ANode in ATree.GetReverseEnumerator do
    DoSomething(ANode);
end;

Extensions FPC

Les exemples de code suivants illustrent les constructions implémentées uniquement dans FPC ; elles ne sont pas supportées par Delphi.

Traversée de types énumération et intervalle

Dans Delphi, il n'est pas possible de traverser non plus les types énumérés ou les intervalles, alors qu'en Free Pascal vous pouvez écrire ce qui suit:

type
  TColor = (clRed, clBlue, clBlack);
  TRange = 'a'..'z';
var
  Color: TColor;
  ch: Char;
begin
  for Color in TColor do
    DoSomething(Color);
  for ch in TRange do
    DoSomethingOther(ch);
end.

Déclarer des énumérateurs

Il n'est pas non plus possible dans Delphi d'ajouter un énumérateur sans modifier la classe, ni d'ajouter un énumérateur d'un type non class object/record/interface. FPC rend cela possible en utilisant la nouvelle syntaxe operator type Enumerator. Comme dans l'exemple suivant:

type
  TMyRecord = record F1: Integer; F2: array of TMyType; end;
  TMyArrayEnumerator = class
  private
    function GetCurrent: TMyType;
  public
    constructor Create(const A: TMyRecord);
    property Current: TMyType read GetCurrent;
    function MoveNext: Boolean;
  end;

  // C'est le nouveau opérateur intégré.
  operator Enumerator(const A: TMyRecord): TMyArrayEnumerator;
  begin
    Result := TMyArrayEnumerator.Create(A);
  end;

var
  A: MyRecord;
  V: TMyType
begin
  for V in A do
    DoSomething(V);
end.

Traversée de chaînes UTF-8

Comme exemple particulièrement utile, l'extension ci-dessous autorise une traversée très efficace des chaînes UTF-8:

uses
  LazUTF8;
interface
type
  { TUTF8StringAsStringEnumerator    
    La traversée de 'codepoint' UTF8 est utile quand vous voulez connaître l'encodage exact d'un 
    caractère UTF-8 ou si vous aimez utiliser des constante de chaîne dans votre code.
    Pour des raisons de sécurité, vous pourriez utiliser les valeurs des points de code (cardinaux) à la place.
    Si la vitesse importe, n'utilisez pas des énumérateurs. Employez à la place directement le PChar
    comme montré dans la méthode MoveNext et lisez sur l'UTF8. Il a des caractéristiques intéressantes. }

  TUTF8StringAsStringEnumerator = class
  private
    fCurrent: UTF8String;
    fCurrentPos, fEndPos: PChar;
    function GetCurrent: UTF8String;
  public
    constructor Create(const A: UTF8String);
    property Current: UTF8String read GetCurrent;
    function MoveNext: Boolean;
  end;

  operator Enumerator(A: UTF8String): TUTF8StringAsStringEnumerator;

var
  Form1: TForm1;

implementation

operator Enumerator(A: UTF8String): TUTF8StringAsStringEnumerator;
begin
  Result := TUTF8StringAsStringEnumerator.Create(A);
end;

{ TUTF8StringAsStringEnumerator }

function TUTF8StringAsStringEnumerator.GetCurrent: UTF8String;
begin
  Result:=fCurrent;
end;

constructor TUTF8StringAsStringEnumerator.Create(const A: UTF8String);
begin
  fCurrentPos:=PChar(A); // Note: si A='' alors PChar(A) retourne un pointeur sur une chaîne #0
  fEndPos:=fCurrentPos+length(A);
end;

function TUTF8StringAsStringEnumerator.MoveNext: Boolean;
var
  l: Integer;
begin
  if fCurrentPos<fEndPos then
  begin
    l:=UTF8CharacterLength(fCurrentPos);
    SetLength(fCurrent,l);
    Move(fCurrentPos^,fCurrent[1],l);
    inc(fCurrentPos,l);
    Result:=true;
  end else
    Result:=false;
end;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
var
  s, ch: UTF8String;
  i: SizeInt;
begin
  s:='mäßig';

  // Utiliser UTF8Length et UTF8Copy de cette manière est très lent, nécessitant O(n)^2
  for i:=1 to UTF8Length(s) do
    writeln('ch=',UTF8Copy(s,i,1));

  // L'emploi de l'énumérateur du dessus est plus court et plutôt rapide, nécessitant O(n)
  for ch in s do
    writeln('ch=',ch);
end;

Utiliser des identificateurs au lieu de MoveNext et Current

En Delphi, vous devez utiliser une fonction avec le nom 'MoveNext' et une propriété avec le nom 'Current' dans les énumérateurs. Avec FPC, vous pouvez choisir n'importe quel nom. Cela est permis par l'emploi du modifieur enumerator, avec la syntaxe des modifieurs 'enumerator MoveNext;' et 'enumerator Current;'. Comme dans l'exemple suivant :

type
  { TMyListEnumerator }

  TMyListEnumerator = object
  private
    FCurrent: Integer;
  public
    constructor Create;
    destructor Destroy;
    function StepNext: Boolean; enumerator MoveNext;
    property Value: Integer read FCurrent; enumerator Current;
  end;

  TMyList = class
  end;

{ TMyListEnumerator }

constructor TMyListEnumerator.Create;
begin
  FCurrent := 0;
end;

destructor TMyListEnumerator.Destroy;
begin
  inherited;
end;

function TMyListEnumerator.StepNext: Boolean;
begin
  inc(FCurrent);
  Result := FCurrent <= 3;
end;

operator enumerator (AList: TMyList): TMyListEnumerator;
begin
  Result.Create;
end;

var
  List: TMyList;
  i: integer;
begin
  List := TMyList.Create;
  for i in List do
    WriteLn(i);
  List.Free;
end.

Extensions proposées

Donner la position de l'énumérateur si disponible

Problématique

Il est impossible d'extraire toute information de l'itérateur sauf l'article courant. Parfois d'autres informations, comme l'index courant, pourraient être utiles :

type
  TUTF8StringEnumerator = class
  private
    FByteIndex: Integer;
    FCharIndex: Integer;
  public
    constructor Create(const A: UTF8String);
    function Current: UTF8Char;
    function CurrentIndex: Integer;
    function MoveNext: Boolean;
  end;

  operator GetEnumerator(A: UTF8String): TUTF8StringEnumerator;
  begin
    Result := TUTF8String.Create(A);
  end;

var
  s: UTF8String;
  ch: UTF8Char;
  i: Integer;
begin

  // inefficace comme discuté plus haut
  for i := 1 to Length(s) do
    Writeln(i, ': ', ch[i]);

  // Ok, mais moche
  i := 1;
  for ch in s do begin
    Writeln(i, ': ', ch);
    Inc(i);
  end;

  // Extension proposée
  for ch in s index i do
    Writeln(i, ': ', ch);

  // Extension proposée pour traverser à l'envers (équivalent au downto)
  for ch in reverse s do
    Writeln(i, ': ', ch);

  // Avec l'extension d'index proposée
  for ch in reverse s index i do
    Writeln(i, ': ', ch);
end.

Remarquez que l'index doit être conçu pour retourner un type arbitraire (c'est-à-dire pas forcément un entier). Par exemple, dans l'exemple d'une traversée d'arbre, l'index pourrait retourner un tableau de nœuds décrivant le chemin allant de la racine au nœud courant.

Solution de contournement pour l'indice

La propriété Current prend la forme d'un couple (Indice,Valeur), on emploie un type générique afin de pérenniser cette conception.

unit IndicedEnumerators;

interface 

type

generic TGIndicedEnumerator<TContainer, TElement> = class
public type
  TIndicedValue = record
    Value: TElement;
    Ind: Integer;
  end;
protected
  FCntnr: TContainer;
  FCurrent: TIndicedValue;
public
  constructor Create(aCntnr: TContainer); virtual;
  function MoveNext: Boolean; virtual; abstract;
  property Current: TIndicedValue read FCurrent;
  function GetEnumerator: TGIndicedEnumerator;
end;

TCsvAnsiStringFieldEnumerator = class(specialize TGIndicedEnumerator<AnsiString, AnsiString>)
private
  FCurrPos: Integer;
  FSeparator: Char;
public
  constructor Create(aString: AnsiString; Separator: AnsiChar); overload;
  function MoveNext: Boolean; override;
end;

implementation

{ TGIndicedEnumerator }

constructor TGIndicedEnumerator.Create(aCntnr: TContainer);
begin
  FCntnr := aCntnr;
end;

function TGIndicedEnumerator.GetEnumerator: TGIndicedEnumerator;
begin
  Result := Self;
end;

{ TCsvAnsiStringFieldEnumerator }

constructor TCsvAnsiStringFieldEnumerator.Create(aString: AnsiString; Separator: AnsiChar);
begin
  inherited Create(aString);
  FCurrPos := 0;
  FSeparator := Separator;
  FCurrent.Ind := -1;
end;

function TCsvAnsiStringFieldEnumerator.MoveNext: Boolean;
begin
  if FCurrPos > Length(FCntnr) then exit(False);
  // Premier élément
  Inc(FCurrent.ind);
  FCurrent.Value := '';
  Inc(FCurrPos);
  while (FCurrPos <= Length(FCntnr)) and (FCntnr[FCurrPos] <> FSeparator) do
  begin
    FCurrent.Value += FCntnr[FCurrPos];
    Inc(FCurrPos);
  end;
  Result := True;
end;

end.

Un point intéressant se trouve dans la déclaration de GetEnumerator dont le type de retour est le type énumérateur lui-même, nécessaire pour pouvoir créer un itérateur à partir de la classe elle-même, mais surtout la méthode sera correctement adaptée lors de la spécialisation et retournera alors le type spécialisé (TCsvAnsiStringFieldEnumerator dans notre exemple). Ce qui montre que la substitution lors de spécialisation ne se limite aux seules marques substitutives ("placeholders").

Dans l'exemple, la spécialisation et la dérivation sont réalisées simultanément mais ce n'est pas une nécessité, il est d'ailleurs sans doute préférable de scinder ces deux opérations et de créer un type intermédiaire spécialisé TAnsiStringEnumerator qui sans introduire la notion de séparateur permettrait de concevoir d'autres itérateurs sur des AnsiString.

L'enregistrement couple (Indice,Valeur) lui-même générique sera automatiquement adapté lors de la spécialisation, cependant, son nom de type devra être qualifié à l'aide du nom de la classe d'énumérateur spécialisé comme nous pouvons le voir dans le petit programme d'exemple, afin de lever toute ambiguïté si deux types spécialisés devaient cohabiter :

#!/usr/bin/instantfpc

// (see the Instantfpc doc)

{$mode objfpc}{$H+}

uses
  SysUtils,
  IndicedEnumerators;  

var
  IndStr: TCsvAnsiStringFieldEnumerator.TIndicedValue;

begin
  // énumérateur obtenu directement à partir de la classe elle-même, la libération de l'énumérateur 
  // est garantie en sortie de la boucle for .. in
  for IndStr in TCsvAnsiStringFieldEnumerator.Create(ParamStr(1), ';') do
    WriteLn(Format('%d : "%s"', [IndStr.Ind, IndStr.Value]));
end.

Appel du script InstantFPC en ligne de commande (nom du fichier source pascal avec des droit d'exécution sous Linux + un paramètre)

 ./TestStringFieldEnums.pas "small;string;with;separated;fields"

Cette solution a l'avantage de mettre à disposition un indice même si celui-ci n'existe pas nativement dans le type traversé, sans avoir à déclarer une variable Indice dans le code client.

Enfin, l'énumérateur générique pourrait être amélioré en ajoutant aux paramètres du constructeur un indice de départ (0 devenant la valeur par défaut).

Référence