Partager via



Avril 2017

Volume 32, numéro 4

Cet article a fait l'objet d'une traduction automatique.

Essential .NET - Présentation des itérateurs personnalisés et des éléments internes foreach C# avec une instructions yield

Par Mark Michaelis

Mark MichaelisCe mois-ci je vais Explorer les détails internes d’une construction de base du langage c# que nous avons tous programmes fréquemment, l’instruction foreach. Compte tenu de comprendre le comportement interne foreach, vous pouvez Explorer ensuite implémenter les interfaces de collection foreach à l’aide de l’instruction yield, comme je l’expliquerai.

Bien que l’instruction foreach est facile à coder, je suis surpris par les développeurs quelques comprennent son fonctionnement en interne. Par exemple, connaissent vous foreach fonctionne différemment pour les tableaux que sur IEnumberable<T> collections ?</T> Quelle est votre connaissance avec la relation entre IEnumerable<T> et IEnumerator<T>?</T> </T> Et, si vous ne comprenez pas les interfaces énumérables, les mettre en œuvre à l’aise utilisez-vous yield ? 

Ce qui rend une classe de Collection

Par définition, une collection dans le Microsoft .NET Framework est une classe qui, au minimum, implémente IEnumerable<T> (ou le type non générique IEnumerable).</T> Cette interface est critique, car l’implémentation des méthodes de IEnumerable<T> est le minimum requis pour prendre en charge l’itération sur une collection.</T>

La syntaxe de l’instruction foreach est simple et évite d’avoir à connaître le nombre d’éléments sont les complications que représente. Le runtime ne directement en charge l’instruction foreach, toutefois. Au lieu de cela, le compilateur c# transforme le code, comme décrit dans les sections suivantes.

foreach avec des tableaux : Le code suivant illustre une boucle foreach simple itération sur un tableau d’entiers et l’impression de chaque entier à la console :

int[] array = new int[]{1, 2, 3, 4, 5, 6};
foreach (int item in array)
{
  Console.WriteLine(item);
}

À partir de ce code, le compilateur c# crée un équivalent CIL de la boucle for :

int[] tempArray;
int[] array = new int[]{1, 2, 3, 4, 5, 6};
tempArray = array;
for (int counter = 0; (counter < tempArray.Length); counter++)
{
  int item = tempArray[counter];
  Console.WriteLine(item);
}

Dans cet exemple, notez que foreach repose sur la prise en charge pour la propriété de longueur et de l’opérateur d’index ([]). Avec la propriété Length, le compilateur c# peut utiliser la pour instruction itérer sur chaque élément du tableau.

foreach avec IEnumerable<T>:</T> Bien que le code précédent fonctionne bien sur les tableaux où la longueur est fixe et l’opérateur index toujours bénéficie, pas tous les types de collections ont un nombre d’éléments connu. En outre, la plupart des classes de collection, y compris la pile<T>, file d’attente<T> et dictionnaire<TKey and="" tvalue="">, ne prennent pas en charge les éléments lors de la récupération par index.</TKey> </T> </T> Par conséquent, une approche plus générale de l’itération sur les collections d’éléments est nécessaire. Le modèle de l’itérateur fournit cette fonctionnalité. En supposant que vous pouvez déterminer le premier, suivants et dernier éléments, connaître le nombre et la prise en charge de la récupération d’éléments par index n’est pas nécessaire.

Le System.Collections.Generic.IEnumerator<T> et interfaces System.Collections.IEnumerator non génériques sont conçus pour activer le modèle de l’itérateur pour itérer sur des collections d’éléments, plutôt que le modèle de longueur-index indiqué précédemment.</T> Un diagramme de classes de leurs relations apparaît dans Figure 1.

Un diagramme de classes, des Interfaces de IEnumerator et IEnumerator
Figure 1 schéma de classe de l’objet IEnumerator<T> et les Interfaces IEnumerator </T>

IEnumerator, quel IEnumerator<T> dérive, contient trois membres.</T> La première est bool MoveNext. Utilisez cette méthode, vous pouvez déplacer à partir d’un élément dans la collection à la suivante, tout en la détection de temps même lorsque vous avez énuméré via chaque élément. Le deuxième membre, une propriété en lecture seule appelée actuel, retourne l’élément actuellement en cours. En cours est surchargé dans IEnumerator<T>en fournissant une implémentation spécifique au type de celui-ci.</T> Avec ces deux membres de la classe de collection, il est possible d’effectuer une itération sur la collection simplement à l’aide d’un certain temps boucle :

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
int number;
// ...
// This code is conceptual, not the actual code.
while (stack.MoveNext())
{
  number = stack.Current;
  Console.WriteLine(number);
}

Dans ce code, la méthode MoveNext renvoie false lorsqu’il se déplace au-delà de la fin de la collection. Cela évite d’avoir à compter les éléments lors de la boucle.

(La méthode Reset généralement lance un NotImplementedException, donc il ne doit jamais être appelée. Si vous devez redémarrer une énumération, créez simplement un nouvel énumérateur.)

L’exemple précédent a montré l’idée de la sortie du compilateur c#, mais il ne compile pas véritablement ainsi car elle omet deux informations importantes concernant la mise en oeuvre : entrelacement et la gestion des erreurs.

L’état est partagé : Le problème avec une implémentation d’un tel que celui de l’exemple précédent est que si deux de ces boucles entrelacent les uns des autres — un foreach dans une autre, les deux à l’aide de la même collection, la collection doit maintenir un indicateur d’état de l’élément actuel afin que lors de l’appel de MoveNext, l’élément suivant peut être déterminé. Dans ce cas, une seule boucle intercalée peut affecter l’autre. (Est de même des boucles exécutées par plusieurs threads.)

Pour résoudre ce problème, les classes de collection ne prennent pas en charge IEnumerator<T> et directement les interfaces IEnumerator.</T> Au lieu de cela, il existe une deuxième interface, appelée IEnumerable<T>, dont la seule méthode est GetEnumerator.</T> L’objectif de cette méthode doit retourner un objet qui prend en charge de IEnumerator<T>.</T> Au lieu de la classe de collection conserver l’état, une autre classe, généralement une classe imbriquée afin qu’il a accès aux éléments internes de la collection : prend en charge l’interface IEnumerator<T> interface et conserve l’état de la boucle d’itération.</T> L’énumérateur est comme un « curseur » ou un « signet » dans la séquence. Vous pouvez avoir plusieurs signets et déplacement d’un d'entre eux énumère la collection indépendamment des autres. À l’aide de ce modèle, l’équivalent c# d’une boucle foreach ressemble à du code illustré Figure 2.

Énumérateur distinct figure 2 Gestion de l’état lors d’une itération

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
int number;
System.Collections.Generic.Stack<int>.Enumerator
  enumerator;
// ...
// If IEnumerable<T> is implemented explicitly,
// then a cast is required.
// ((IEnumerable<int>)stack).GetEnumerator();
enumerator = stack.GetEnumerator();
while (enumerator.MoveNext())
{
  number = enumerator.Current;
  Console.WriteLine(number);
}

Nettoyage de l’itération suivante : Étant donné que les classes qui implémentent l’interface IEnumerator<T> interface maintenir l’état, vous devez parfois nettoyer l’état une fois qu’il quitte la boucle (étant donné que toutes les itérations terminées ou une exception est levée).</T> Pour ce faire, IEnumerator<T> interface dérive de IDisposable.</T> Énumérateurs qui implémentent IEnumerator nécessairement n’implémentent pas IDisposable, mais le cas, Dispose sera appelée, également. Ainsi, l’appel de Dispose lorsque l’utilisateur quitte la boucle foreach. L’équivalent c# du code CIL final, par conséquent, ressemble à Figure 3.

Figure 3 les résultats compilés de foreach sur des Collections

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
System.Collections.Generic.Stack<int>.Enumerator
  enumerator;
IDisposable disposable;
enumerator = stack.GetEnumerator();
try
{
  int number;
  while (enumerator.MoveNext())
  {
    number = enumerator.Current;
    Console.WriteLine(number);
  }
}
finally
{
  // Explicit cast used for IEnumerator<T>.
  disposable = (IDisposable) enumerator;
  disposable.Dispose();
  // IEnumerator will use the as operator unless IDisposable
  // support is known at compile time.
  // disposable = (enumerator as IDisposable);
  // if (disposable != null)
  // {
  //   disposable.Dispose();
  // }
}

Notez que, étant donné que l’interface IDisposable est pris en charge par IEnumerator<T>, l’instruction using peut simplifier le code dans Figure 3 à celui présenté dans Figure 4.</T>

Figure 4 une gestion d’erreurs et de nettoyage à l’utilisation des ressources

System.Collections.Generic.Stack<int> stack =
  new System.Collections.Generic.Stack<int>();
int number;
using(
  System.Collections.Generic.Stack<int>.Enumerator
    enumerator = stack.GetEnumerator())
{
  while (enumerator.MoveNext())
  {
    number = enumerator.Current;
    Console.WriteLine(number);
  }
}

Toutefois, souvenez-vous que le code CIL ne gère pas directement à l’aide de la (mot clé). Par conséquent, le code dans la Figure 3 est en fait une représentation plus précise c# du code CIL foreach.

foreach sans IEnumerable : C# ne nécessite pas que IEnumerable/IEnumerable<T> être implémentée pour effectuer une itération sur un type de données à l’aide de foreach.</T> Au lieu de cela, le compilateur utilise un concept appelé canard saisie ; Il recherche une méthode GetEnumerator qui retourne un type avec une propriété Current et une méthode MoveNext. Canard en tapant implique la recherche par nom plutôt que de compter sur une interface ou un appel de méthode explicite à la méthode. (Le nom « canard en tapant » vient de l’idée originaux pour être considérées comme un canard, l’objet doit implémenter simplement une méthode Quack ; il ne peut-être pas implémenter une interface IDuck). Si canard en tapant ne parvient pas à trouver une implémentation du modèle énumérable appropriée, le compilateur vérifie si la collection implémente les interfaces.

Présentation des itérateurs

Maintenant que vous comprenez les mécanismes internes de l’implémentation de foreach, il est temps pour décrire comment les itérateurs sont utilisés pour créer des implémentations personnalisées de l’objet IEnumerator<T>, IEnumerable<T> et non générique correspondante des interfaces pour les collections personnalisées.</T> </T> Itérateurs fournissent une syntaxe claire pour spécifier comment effectuer une itération sur les données dans les classes de collection, en particulier à l’aide de la boucle foreach, permettant aux utilisateurs d’une collection naviguer dans sa structure interne sans avoir connaissance de cette structure.

Le problème avec le modèle d’énumération est qu’il peut être difficile à implémenter manuellement, car il doit conserver tous les États nécessaires décrire la position actuelle dans la collection. Cet état interne peut être simple, une classe de type de collection liste ; l’index de la position actuelle est suffisante. En revanche, les structures de données nécessitant une traversée récursive, tels que des arborescences binaires, l’état peut être relativement complexe. Pour atténuer les défis liés à l’implémentation de ce modèle, c# 2.0 ajouté le mot clé contextuel yield pour faciliter une classe déterminent comment la boucle foreach effectue une itération sur son contenu.

Définition d’un itérateur : itérateurs sont un moyen d’implémenter les méthodes d’une classe, et elles sont raccourcis syntaxiques pour le modèle d’énumérateur plus complexe. Lorsque le compilateur c# rencontre un itérateur, elle développe son contenu en code CIL qui implémente le modèle d’énumérateur. Par conséquent, il n’existe aucune dépendance de runtime pour l’implémentation des itérateurs. Étant donné que le compilateur c# gère la mise en œuvre via la génération de code CIL, il ne fournit aucun avantage performances runtime réel à l’aide d’itérateurs. Toutefois, il existe un gain de productivité de programmeur substantielle dans le choix des itérateurs sur une implémentation du modèle d’énumérateur. Pour comprendre cette amélioration, allez tout d’abord prendre en compte comment un itérateur est défini dans le code.

Syntaxe de l’itérateur : Un itérateur fournit une implémentation de raccourci d’interfaces d’itérateur, la combinaison de l’interface IEnumerable<T> et IEnumerator<T> interfaces.</T> </T> Figure 5 déclare un itérateur pour la BinaryTree générique<T> type en créant une méthode GetEnumerator (mais, encore sans implémentation).</T>

Figure 5 itérateur Interfaces modèle

using System;
using System.Collections.Generic;
public class BinaryTree<T>:
  IEnumerable<T>
{
  public BinaryTree ( T value)
  {
    Value = value;
  }
  #region IEnumerable<T>
  public IEnumerator<T> GetEnumerator()
  {
    // ...
  }
  #endregion IEnumerable<T>
  public T Value { get; }  // C# 6.0 Getter-only Autoproperty
  public Pair<BinaryTree<T>> SubItems { get; set; }
}
public struct Pair<T>: IEnumerable<T>
{
  public Pair(T first, T second) : this()
  {
    First = first;
    Second = second;
  }
  public T First { get; }
  public T Second { get; }
  #region IEnumerable<T>
  public IEnumerator<T> GetEnumerator()
  {
    yield return First;
    yield return Second;
  }
  #endregion IEnumerable<T>
  #region IEnumerable Members
  System.Collections.IEnumerator
    System.Collections.IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }
  #endregion
  // ...
}

Ce qui produit des valeurs à partir d’un itérateur : Les interfaces d’itérateur sont comme les fonctions, mais au lieu de retourner une valeur unique, ils produisent une séquence de valeurs, un à la fois. Dans le cas de BinaryTree<T>, l’itérateur génère une séquence de valeurs de l’argument de type fourni pour T. Si la version non générique de IEnumerator est utilisée, les valeurs rapportés sera de type object.</T>

Pour implémenter correctement le modèle de l’itérateur, vous devez maintenir un état interne pour effectuer le suivi de votre avancement lors de l’énumération de la collection. Dans le BinaryTree<T> cas, suivre les éléments de l’arborescence ont déjà été énumérés et qui sont encore à venir.</T> Les itérateurs sont transformés par le compilateur dans une machine à états « » qui effectue le suivi de la position actuelle et sait comment « déplacer lui-même » à la position suivante.

L’instruction yield return renvoie une valeur chaque fois qu’un itérateur rencontre contrôle retourne immédiatement à l’appelant qui a demandé l’élément. Lorsque l’appelant demande l’élément suivant, le code commence à exécuter immédiatement après le rendement exécuté précédemment instruction return. Dans Figure 6, les mots-clés de type de données intégrées c# sont retournés séquentiellement.

Figure 6 générant de manière séquentielle certains mots clés c#

using System;
using System.Collections.Generic;
public class CSharpBuiltInTypes: IEnumerable<string>
{
  public IEnumerator<string> GetEnumerator()
  {
    yield return "object";
    yield return "byte";
    yield return "uint";
    yield return "ulong";
    yield return "float";
    yield return "char";
    yield return "bool";
    yield return "ushort";
    yield return "decimal";
    yield return "int";
    yield return "sbyte";
    yield return "short";
    yield return "long";
    yield return "void";
    yield return "double";
    yield return "string";
  }
    // The IEnumerable.GetEnumerator method is also required
    // because IEnumerable<T> derives from IEnumerable.
  System.Collections.IEnumerator
    System.Collections.IEnumerable.GetEnumerator()
  {
    // Invoke IEnumerator<string> GetEnumerator() above.
    return GetEnumerator();
  }
}
public class Program
{
  static void Main()
  {
    var keywords = new CSharpBuiltInTypes();
    foreach (string keyword in keywords)
    {
      Console.WriteLine(keyword);
    }
  }
}

Les résultats de Figure 6 apparaissent dans Figure 7, qui est une liste des types c# intégrés.

Liste figure 7 d’une sortie de mots clés c# à partir du Code dans la Figure 6

object
byte
uint
ulong
float
char
bool
ushort
decimal
int
sbyte
short
long
void
double
string

Clairement, explications supplémentaires est obligatoire, mais je n’ai pas d’espace pour ce mois-ci donc je vous laisse en attente d’une autre colonne. Je me contenterais de dire, avec les itérateurs vous pouvez comme par magie créer collections en tant que propriétés, comme indiqué dans Figure 8: dans ce cas, compter sur les tuples c# 7.0 juste pour le plaisir de celui-ci. Pour ceux d'entre vous qui veulent effectuer une préanalyse, vous pouvez extraire le code source ou un coup de œil au chapitre 16 de mon livre « Essential c# ».

Figure 8 Utilisation yield revenir à implémenter une<T> propriété</T> de IEnumerable

IEnumerable<(string City, string Country)> CountryCapitals
{
  get
  {
    yield return ("Abu Dhabi","United Arab Emirates");
    yield return ("Abuja", "Nigeria");
    yield return ("Accra", "Ghana");
    yield return ("Adamstown", "Pitcairn");
    yield return ("Addis Ababa", "Ethiopia");
    yield return ("Algiers", "Algeria");
    yield return ("Amman", "Jordan");
    yield return ("Amsterdam", "Netherlands");
    // ...
  }
}

Pour résumer

Dans cet article, j’en escalier à fonctionnalité qui fait partie du langage c# depuis la version 1.0 et n’a pas beaucoup changé depuis l’introduction de génériques en c# 2.0. Malgré l’utilisation fréquente de cette fonctionnalité, toutefois, nombreuses ne comprennent pas les détails de ce qui se déroule en interne. Je puis effleurer le motif de l’itérateur, en exploitant le rendement retourne construction — et fourni un exemple.

De cette colonne a été extrait de mon livre « Essential c# » (IntelliTect.com/EssentialCSharp), dont je suis actuellement en cours de mise à jour dans « Essential c# 7.0. » Pour plus d’informations, consultez les chapitres 14 et 16.


Mark Michaelisest le fondateur de IntelliTect, où il sert de son poste d’architecte en chef technique et un formateur.  Depuis près de 20 ans, il a été MVP Microsoft et directeur régional Microsoft depuis 2007. Michaelis fait plusieurs logiciels conception révision équipes Microsoft, notamment c#, Microsoft Azure, SharePoint et Visual Studio ALM. Il participe à des conférences de développeurs et a écrit de nombreux ouvrages, y compris sa plus récente, « Essential c# 6.0 (5e édition) » (itl.tc/EssentialCSharp). Contactez-le sur Facebook à facebook.com/Mark.Michaelis, sur son blog à l’adresse IntelliTect.com/Mark, sur Twitter : @markmichaelis ou par courrier électronique à mark@IntelliTect.com.

Je remercie les experts techniques IntelliTect suivants d’avoir relu cet article : Kevin Bost, Grant Erickson, Chris Finlayson, Phil Spokas et Michael Stokesbary