Vous avez dit « itérateur »?

Si vous avez déjà codé plus de 10 lignes de C# dans votre vie, vous avez nécessairement utilisé l’opérateur « foreach ». Injustement interprété par certains néophytes comme une facilité syntaxique pour réaliser un « for » sur tous les indexes d’une collection, l’opérateur « foreach » implémente en réalité le pattern itérateur bien connu des adeptes du GoF.

Cette instruction « magique » du langage C# fonctionne en effet parfaitement avec les collections ne supportant pas l’accès par index, puisque sa transcription vers IL par le compilateur révèle non pas un parcours par index, mais la délégation de l’itération à une classe « IEnumerator » (« IEnumerator<T> » pour les types génériques) et l’appel de la méthode IEnumerator.MoveNext() à chaque passage de la boucle.

Très grossièrement, l’instruction suivante:
(note: suite à des soucis d’affichage, tous les « < » et « > » présents dans les extraits de code ci-dessous ont été remplacés respectivement par « [ » et « ] »)

est compilée en:

Toute la logique de l’énumérateur se trouvant dans le « MoveNext() » propre au type itéré, qui peut ne pas être un simple incrément d’index.

Admettez en tout cas que la première syntaxe est tout de même plus sexy…

Pour que cette compilation fonctionne, il faut a minima que le type « itéré » expose une méthode « GetEnumerator() » dont le type de retour est IEnumerator (ou IEnumerator<T>), et dans l’idéal (dans un souci d’interopérabilité entre langages .net) qu’il implémente l’interface IEnumerable (ou IEnumerable<T>) déclarant précisément cette même méthode.

Implémenter votre propre classe-collection pour la rendre consommable par un « foreach » nécessite donc de lui faire implémenter l’interface « IEunmerable » ou « IEnumerable<T> » et son unique méthode « GetEnumerator() », ainsi que le type « IEnumerator » (ou « IEnumerator<T> ») fournissant la logique d’itération.

De là, deux routes s’offrent à vous:

  • La méthode old-school où vous fournissez manuellement l’implémentation de IEnumerator/IEnumerator<T> et de ses méthodes « Current() » et « MoveNext() ».
  • La méthode « magique » où vous laissez le compilateur faire le travail à votre place à l’aide d’un « itérateur » construit autour du mot clef « yield » introduit dans la spécification 2.0 du langage C# (visual studio 2005).

Si la méthode manuelle donnera l’impression de parfaitement maîtriser son code (et les bugs qu’on souhaite y intégrer), la méthode magique séduira les plus hardis par la simplicité de son expression.

Imaginez la collection générique suivante:

Soit une liste simplement chaînée d’éléments du type T. L’implémentation manuelle de sa méthode « GetEnumerator() » ressemblerait à ça:

Je vous passe l’implémentation complète de l’IEnumerator, il y en a encore pour quelques dizaines de lignes dans les cas les plus simples…
En revanche, avec le mot clef yield, cette même méthode ressemble à ceci:

Là où réside toute la beauté du « yield », c’est que le compilateur est capable, à partir de ces simples lignes, de générer le tout code nécessaire au fonctionnement d’un foreach. La seule contrainte est que la méthode contenant un « yield return » ait un type retour IEnumerable/IEnumerable<T> ou IEnumerator/IEnumerator<T> et que l’expression suivant le « yield return » soit implicitement convertible en T.

« yield » ajoute donc une deuxième couche de magie par dessus celle du « foreach » (qui vous épargne l’écriture du « comment consommer mon itérable ») en écrivant à votre place le « comment itérer mon itérable ».

En faisant le parallèle avec l’implémentation compilo du « foreach » définie en début de billet, on comprend que L’appel à une méthode contenant un « yield return » n’exécute pas, à proprement dit le code de la méthode « itérateur ». La première étape consiste en effet à récupérer l’instance de IEnumerator consommable par le foreach, et cette étape est fournie par une partie du code généré par le compilateur. Le code de votre méthode n’est effectivement exécuté qu’à la consommation de l’IEnumerator par « MoveNext() ». A chaque boucle du foreach, le code de votre méthode « itérateur » va ainsi s’exécuter jusqu’à l’instruction « yield return » dont le résultat constituera le « Current » du IEnumerator, puis se « suspendre » jusqu’à la boucle suivante du « foreach » où l’exécution de la méthode reprendra là où elle avait été laissée.

En réalité, une fois compilé vers IL, il n’y a -tout comme avec await/async- rien de magique. La méthode contenant le « yield return » a bel et bien été décomposée en un GetEnumerator() et une implémentation de IEnumerator et de ses méthodes Current et MoveNext() consommables par le « foreach » et reproduisant la logique de votre code. La seule différence résidant dans la robustesse de cette implémentation et la lisibilité de votre code.

Mais les possibilités du « yield » ne s’arrêtent pas à la simplification de vos collections IEnumerable. En effet, à la place d’une instance de votre collection IEnumerable, vous pouvez logiquement passer à « foreach » n’importe quelle méthode, opérateur ou propriété de lecture dont le type de retour est IEnumerable/IEnumerable<T> et même IEnumerator/IEnumerator<T> (auquel cas celui-ci sera consommé dans passer par l’étape « GetEnumerator() »). Or, puisque l’instruction « yield » se trouve justement dans une méthode dont le type de retour est précisément l’un de ces 4 derniers, et implémente pour vous toute la logique de ces types, vous pouvez créer des méthodes, opérateurs ou propriétés de lecture itérables au sein de n’importe quelle classe, sans implémenter manuellement la moindre interface!

Je vous laisse imaginer la complexité du code nécessaire pour reproduire « manuellement » (sans « yield » ni « foreach ») le fonctionnel décrit par ces quelques lignes…

De plus, de telles méthodes itérables peuvent être consommées non seulement par l’instruction foreach, mais aussi par les requêtes LINQ. Ce qui agrandit encore considérablement leur périmètre utile.

Enfin, je clos ce billet en portant à votre attention l’instruction « yield break » dont l’exécution aura pour effet de mettre un terme à l’itération.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

3 + 7 =