|
|
Les itérateurs de C#2 par Patrick
Smacchia |
Comprendre
les concepts d’énumérable et d’énumérateur
Plusieurs
itérateurs sur une même classe
Problèmes
avec les itérateurs de C#1
Un
premier exemple avec le mot-clé yield return
Les
itérateurs et la généricité
Plusieurs
itérateurs pour une même classe conteneur
Contraintes
syntaxique imposées par l’utilisation des mot-clés yield return et yield
break
Exemple d’un itérateur
récursif
Interprétation
des itérateurs par le compilateur de C#2
La
classe énumérateur est implémentée automatiquement par le compilateur
Une
machine à état est fabriquée pour chaque énumérateur
Exemples
avancés de l’utilisation des itérateurs de C#2
Définitions :
coroutine et de continuation
Un
exemple de continuation avec les itérateurs
Méthodes
anonymes, itérateurs et foncteurs
Une
limitation des itérateurs C#2
Le seul
pré requis pour aborder la partie ‘avancée’ de cet article (i.e à
partir de Interprétation des itérateurs par le compilateur de C#2)
est d’avoir lu l’article
précédent consacré aux méthodes anonymes.
Après avoir disséqué les méthodes anonymes de C#2, il est temps de procéder à une analyse minutieuse des itérateurs. Rappelons que pour cette fonctionnalité aussi, aucune instruction IL n’a été rajoutée à la version 2 de .NET. Toute la magie se situe au niveau du compilateur. Nous nous intéresserons tout d’abord au itérateurs de C#1 pour bien comprendre les motivations qui ont poussé à l'amélioration de cette partie du langage. Après une présentation ‘classique’ de la fonctionnalité proprement dite, nous analyserons minutieusement le travail du compilateur pour enfin, commenter des utilisations avancées.
Avant de nous lancer dans un exemple d’utilisation des itérateurs en C#1, il est nécessaire de comprendre les concepts d’énumérable et d’énumérateur.
On dit d’un objet qu’il est énumérable s'il constitue lui-même une collection d’objets et si on peut énumérer cette collection au moyen du mot-clé foreach. Concrètement, une condition suffisante (mais pas forcément nécessaire) pour qu’un objet soit énumérable est qu’il implémente l’interface System.Collections.IEnumerable.
On dit d’un objet que c’est un énumérateur s’il implémente l’interface System.Collections.IEnumerator.
Voici la définition de ces deux interfaces :
namespace System.Collections{
/// <summary>Exposes the enumerator, which supports a simple iteration
over a non-generic collection.</summary>
/// <filterpriority>2</filterpriority>
[System.Runtime.InteropServices.GuidAttribute("496B0ABE-CDEE-11d3-88E8-00902754C43A")]
public
interface IEnumerable{
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> that can be used to
iterate through the collection.</returns>
/// <filterpriority>2</filterpriority>
[System.Runtime.InteropServices.DispIdAttribute(252)]
System.Collections.IEnumerator GetEnumerator();
}
/// <summary>Supports a simple iteration over a non-generic collection.</summary>
/// <filterpriority>2</filterpriority>
[System.Runtime.InteropServices.GuidAttribute("496B0ABF-CDEE-11d3-88E8-00902754C43A")]
public
interface IEnumerator{
/// <summary>Gets the current element in the collection.</summary>
/// <returns>The current element in the collection.</returns>
/// <filterpriority>2</filterpriority>
object
Current { get; }
/// <summary>Advances the enumerator to the next element of the
collection.</summary>
/// <returns>true if the enumerator was successfully advanced to the next
element; false, if the enumerator has passed the end of the collection.</returns>
/// <filterpriority>2</filterpriority>
bool
MoveNext();
//Sets the enumerator to its initial position,
which is before the first element in the collection.</summary>
/// <filterpriority>2</filterpriority>
void
Reset();
}
}
En analysant ces interfaces, on comprend qu’un client qui veut énumérer la collection d’objets détenue par un objet énumérable, demande à ce dernier un énumérateur au moyen de la méthode IEnumerator IEnumerable.GetEnumerator(). Ensuite, le client peut utiliser les méthodes IEnumerator.MoveNext() et IEnumerator.Reset() sur l’énumérateur retourné pour déplacer l’index courant dans la collection d’objet. Lorsqu’il veut obtenir un l’objet indexé, le client appelle l’accesseur get de la propriété Current. Dans un diagramme UML cela donne :

Si vous consultez maintenant votre GoF (Gang of Four, Design Pattern), vous vous apercevez que l’on vient de décrire le design pattern itérator.
Voici un exemple d’implémentation des
itérateurs en C#1. La classe Personnes
joue le rôle d’énumérable tandis que la classe PersonnesEnumerator joue le rôle de
l’énumérateur. Notez que la classe Personnes
aurait pu jouer à la fois le rôle de l’énumérable et de
l’énumérateur. Il aurait suffit qu’elle implémente en plus
l’interface IEnumerator.
Il est préférable d’isoler l’énumérateur dans une classe tierce
dédiée afin de, comme nous allons le voir, pouvoir implémenter plusieurs
énumérateurs pour un même énumérable :
using System;
using System.Collections;
public class Personnes : IEnumerable{
private class PersonnesEnumerator : IEnumerator{
private int index =
-1;
private Personnes P;
public PersonnesEnumerator(Personnes P){ this.P
= P; }
public bool
MoveNext(){
index++;
return index < P.m_Noms.Length;
}
public void Reset(){ index
= -1; }
public object
Current{ get { return
P.m_Noms[index]; } }
}
// la méthode GetEnumerator() de IEnumerable
public IEnumerator
GetEnumerator(){
return new PersonnesEnumerator(this);
}
string[] m_Noms;
// le constructeur qui initialise le tableau
public Personnes(params string[]
Noms){
m_Noms = new string[Noms.Length];
// Copie le tableau.
Noms.CopyTo(m_Noms,
0);
}
// l'indexeur qui retourne le Nom à partir de l'index
private string
this[int
index]{
get { return
m_Noms[index]; }
set { m_Noms[index] = value; }
}
}
class Program{
static void
Personnes arrPersonnes = new Personnes("Michel", "Christine",
"Mathieu", "Julien");
foreach (string s in arrPersonnes)
Console.WriteLine(s);
Console.ReadLine();
}
}
Un peu lourd pour une telle fonctionnalité, n’est ce pas ?! Heureusement que C#2 simplifie ceci.
Ce programme affiche:
Michel
Christine
Mathieu
Julien
Il est clair que le compilateur C# a interprété le
mot-clé foreach comme
ceci :
...
class Program{
static void
Personnes arrPersonnes = new Personnes("Michel", "Christine",
"Mathieu", "Julien");
IEnumerator e =
arrPersonnes.GetEnumerator();
while (e.MoveNext())
Console.WriteLine((string)e.Current);
Console.ReadLine();
}
}
En fait, l’énumérable n’est pas obligé d’implémenter l’interface IEnumerable. On peut déléguer cette responsabilité à une classe tierce, PersonnesEnumerable par exemple. Voici le programme précédent réécrit :
using System;
using System.Collections;
public class
Personnes // n'implémente pas IEnumerable{
private class PersonnesEnumerator : IEnumerator{
...
}
private class PersonnesEnumerable : IEnumerable{
private Personnes m_Personnes;
internal PersonnesEnumerable(Personnes
personnes) { m_Personnes = personnes; }
IEnumerator IEnumerable.GetEnumerator(){
return new PersonnesEnumerator(m_Personnes);
}
}
public IEnumerable
InOrder{ get { return
new PersonnesEnumerable(this); } }
...
}
class Program{
static void
Personnes arrPersonnes = new Personnes("Michel", "Christine",
"Mathieu", "Julien");
foreach (string s in arrPersonnes.InOrder)
Console.WriteLine(s);
Console.ReadLine();
}
}
On s’aperçoit que de cette façon il devient possible d’implémenter plusieurs énumérateurs pour notre classe Personnes. Par exemple un énumérateur Reverse pour parcourir la collection de personnes à l’envers ou bien un énumérateur