Les itérateurs de C#2 par Patrick Smacchia

 

Prés requis. 1

Introduction. 1

Les itérateurs de C#1. 1

Comprendre les concepts  d’énumérable et d’énumérateur 1

Un exemple. 1

Plusieurs itérateurs sur une même classe. 2

Problèmes avec les itérateurs de C#1. 2

Les itérateurs avec C#2. 2

Un premier exemple avec le mot-clé yield return. 2

Les itérateurs et la généricité. 2

Plusieurs itérateurs pour une même classe conteneur 3

Le mot-clé yield break. 3

Contraintes syntaxique imposées par l’utilisation des mot-clés yield return et yield break. 3

Exemple d’un itérateur récursif 3

Interprétation des itérateurs par le compilateur de C#2. 4

La classe énumérateur est implémentée automatiquement par le compilateur 4

Une machine à état est fabriquée pour chaque énumérateur 5

Exemples avancés de l’utilisation des itérateurs de C#2. 5

Définitions : coroutine et de continuation. 5

Un exemple de continuation avec les itérateurs. 5

La pattern Pipeline. 6

Méthodes anonymes, itérateurs et foncteurs. 6

Continuation vs. Threading. 6

Une limitation des itérateurs C#2. 7

Conclusion. 7

 

Pré-requis

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. 10) est d’avoir lu l’article précédent consacré aux méthodes anonymes.

Introduction

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.

Les itérateurs de C#1

Comprendre les concepts  d’énumérable et d’énumérateur

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.

Un exemple

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 Main(string[] args){

      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 Main(string[] args){

      Personnes arrPersonnes = new Personnes("Michel", "Christine", "Mathieu", "Julien");

      IEnumerator e = arrPersonnes.GetEnumerator();

      while (e.MoveNext())

         Console.WriteLine((string)e.Current);

      Console.ReadLine();

   }

}

Plusieurs itérateurs sur une même classe

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 Main(string[] args){

      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