Juin 2017
Volume 32, numéro 6
Cet article a fait l'objet d'une traduction automatique.
L’essentiel de .NET - Itérateurs personnalisés avec Yield
Par marque Michaelis
Dans mon dernier article (msdn.com/magazine/mt797654), je s’intéresse dans les détails du fonctionne de l’instruction foreach c# en arrière-plan, qui explique comment le compilateur c# implémente les fonctionnalités de foreach dans langage CIL (Common Intermediate). J’ai également brièvement abordé le mot clé de génération avec un exemple (voir Figure 1), mais il y a peu, voire aucune explication.
Figure 1 générant de façon séquentielle certains mots clés c#
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);
}
}
}
Il s’agit d’une continuation de cet article, dans lequel fournir plus de détails sur le mot clé de génération et comment l’utiliser.
Itérateurs et l’état
En plaçant un point d’arrêt au début de la méthode GetEnumerator dans Figure 1, vous pouvez observer que GetEnumerator est appelée au début de l’instruction foreach. À ce stade, un objet itérateur est créé et son état est initialisé à une spéciale « start » état qui représente le fait qu’aucun code n’exécuté dans l’itérateur et, par conséquent, aucune valeur n’aient été retournés encore. Dès lors, l’itérateur conserve son état de (emplacement), tant que l’instruction foreach au site d’appel continue de s’exécuter. Chaque fois que la boucle demande la valeur suivante, le contrôle passe à l’itérateur et continue où elle s’est arrêté lors du précédent dans la boucle ; les informations d’état stockées dans l’objet itérateur sont utilisées pour déterminer où le contrôle doit reprendre. Lorsque l’instruction foreach au site d’appel s’arrête, état de l’itérateur n’est plus enregistré. Figure 2 affiche un diagramme de séquence de haut niveau de ce qui se passe. N’oubliez pas que la méthode MoveNext apparaît sur l’interface IEnumerator < T >.
Dans Figure 2, l’instruction foreach au site d’appel lance un appel à GetEnumerator sur l’instance CSharpBuiltInTypes appelé des mots clés. Comme vous pouvez le voir, il est toujours possible d’appeler GetEnumerator. objets de l’énumérateur « nouvelle » seront créés lorsque cela est nécessaire. Étant donné l’instance de l’itérateur (référencé par l’itérateur), foreach commence chaque itération avec un appel à MoveNext. Dans l’itérateur, vous de générer une valeur à l’instruction foreach au site d’appel. Une fois que l’instruction yield return, la méthode GetEnumerator apparemment exécution s’interrompt jusqu'à la prochaine requête MoveNext. Dans le corps de la boucle, l’instruction foreach affiche la valeur rapportée sur l’écran. Il retour de boucle et appelle de nouveau MoveNext sur l’itérateur. Notez que la deuxième fois, contrôle récupère à la deuxième instruction yield return. Une fois encore, foreach affiche à l’écran, ce qui générait CSharpBuiltInTypes et redémarre la boucle. Ce processus se poursuit jusqu'à ce que plus aucun plusieurs instructions yield return de l’itérateur. À ce stade, la boucle foreach sur le site d’appel s’arrête car MoveNext retourne false.
Figure 2 diagramme de séquence avec Yield Return
Un autre exemple d’itérateur
Prenons un exemple similaire avec le BinaryTree < T >, que j’ai présentée dans l’article précédent. Pour implémenter le BinaryTree < T >, j’ai besoin d’abord paire < T > pour prendre en charge l’interface IEnumerable < T > à l’aide d’un itérateur. Figure 3 est un exemple qui retourne chaque élément de la paire < T >.
Dans Figure 3, l’itération sur les données de la paire < T > type boucles à deux reprises : tout d’abord par yield renvoyer premier et puis via yield seconde. Chaque fois que le rendement de retour dans GetEnumerator est rencontrée, l’état est enregistré et s’affiche de l’exécution de « sauter » hors du contexte de la méthode GetEnumerator et dans le corps de la boucle. Démarrage de la deuxième itération, GetEnumerator commence à s’exécuter à nouveau avec l’instruction de deuxième yield return.
Figure 3 à l’aide de rendement pour implémenter BinaryTree < T >
public struct Pair<T>: IPair<T>,
IEnumerable<T>
{
public Pair(T first, T second) : this()
{
First = first;
Second = second;
}
public T First { get; } // C# 6.0 Getter-only Autoproperty
public T Second { get; } // C# 6.0 Getter-only Autoproperty
#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
}
Implémentation d’IEnumerable avec IEnumerable < T >
Pas ' System.Collections.IEnumerable ' hérite de System.Collections.Generic.IEnumerable < T >. Par conséquent, lors de l’implémentation IEnumerable < T >, il est également nécessaire implémenter IEnumerable. Dans Figure 3, il est fait explicitement et l’implémentation implique simplement un appel à l’implémentation IEnumerable < T > GetEnumerator. Cet appel IEnumerable.GetEnumerator en IEnumerable < T >. GetEnumerator fonctionne toujours en raison de la compatibilité du type entre IEnumerable < T > et IEnumerable (via l’héritage). Étant donné que les signatures pour les deux GetEnumerators sont identiques (le type de retour ne distinguer une signature), un ou deux implémentations doivent être explicites. Étant donné la sécurité de type supplémentaires offerte par la version de IEnumerable < T >, l’implémentation IEnumerable doit être explicite.
Le code suivant utilise la paire < T >. Méthode GetEnumerator et affiche « Inigo » et « Montoya » sur deux lignes consécutives :
var fullname = new Pair<string>("Inigo", "Montoya");
foreach (string name in fullname)
{
Console.WriteLine(name);
}
Placement Yield Return dans une boucle
Il n’est pas nécessaire de coder en dur chaque instruction yield return, comme je le faisais dans CSharpPrimitiveTypes et paire < T >. À l’aide de l’instruction yield return, vous pouvez renvoyer à partir d’à l’intérieur d’une construction de la boucle. Figure 4 utilise une boucle foreach. Chaque fois que le foreach dans GetEnumerator s’exécute, elle retourne la valeur suivante.
La figure 4, placez les instructions Yield Return dans une boucle
public class BinaryTree<T>: IEnumerable<T>
{
// ...
#region IEnumerable<T>
public IEnumerator<T> GetEnumerator()
{
// Return the item at this node.
yield return Value;
// Iterate through each of the elements in the pair.
foreach (BinaryTree<T> tree in SubItems)
{
if (tree != null)
{
// Because each element in the pair is a tree,
// traverse the tree and yield each element.
foreach (T item in tree)
{
yield return item;
}
}
}
}
#endregion IEnumerable<T>
#region IEnumerable Members
System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
Dans Figure 4, la première itération retourne l’élément racine dans l’arborescence binaire. Au cours de la deuxième itération, vous parcourez la paire de sous-éléments. Si la paire de sous-élément contienne une valeur non null, vous Parcourir ce nœud enfant et génère ses éléments. Notez que foreach (élément T dans l’arborescence) est un appel récurrent à un nœud enfant.
Comme observée avec CSharpBuiltInTypes et paire < T >, vous pouvez désormais itérer BinaryTree < T > à l’aide d’une boucle foreach. Figure 5 illustre ce processus.
Foreach à l’aide de la figure 5 avec BinaryTree < chaîne >
// JFK
var jfkFamilyTree = new BinaryTree<string>(
"John Fitzgerald Kennedy");
jfkFamilyTree.SubItems = new Pair<BinaryTree<string>>(
new BinaryTree<string>("Joseph Patrick Kennedy"),
new BinaryTree<string>("Rose Elizabeth Fitzgerald"));
// Grandparents (Father's side)
jfkFamilyTree.SubItems.First.SubItems =
new Pair<BinaryTree<string>>(
new BinaryTree<string>("Patrick Joseph Kennedy"),
new BinaryTree<string>("Mary Augusta Hickey"));
// Grandparents (Mother's side)
jfkFamilyTree.SubItems.Second.SubItems =
new Pair<BinaryTree<string>>(
new BinaryTree<string>("John Francis Fitzgerald"),
new BinaryTree<string>("Mary Josephine Hannon"));
foreach (string name in jfkFamilyTree)
{
Console.WriteLine(name);
}
Et Voici les résultats :
John Fitzgerald Kennedy
Joseph Patrick Kennedy
Patrick Joseph Kennedy
Mary Augusta Hickey
Rose Elizabeth Fitzgerald
John Francis Fitzgerald
Mary Josephine Hannon
L’origine des itérateurs
En 1972, Barbara Liskov et une équipe de chercheurs de MIT a commencé à recherche sur les méthodologies de programmation, en mettant l’accent sur les abstractions de données défini par l’utilisateur. Pour prouver une grande partie de leur travail, ils créé un langage appelé CLU ayant un concept appelé « clusters » (CLU en cours des trois premières lettres de ce terme). Clusters ont été prédécesseurs à l’abstraction de données primaire que les programmeurs utiliser aujourd'hui : objets. Lors de leur recherche, l’équipe de réaliser que même si elles ont été en mesure d’utiliser le langage CLU abstraire une représentation de données en dehors des utilisateurs finaux de leurs types, ils constamment trouvant eux-mêmes faire apparaître la structure interne des données pour permettre aux utilisateurs de consommer intelligemment. Le résultat de leur grande consternation parmi a été la création d’une construction de langage appelée un itérateur. (La langue CLU proposés nombreux insights en ce qui est finalement être popularisé en tant que « programmation orientée objet ».)
L’annulation d’autre itération : Yield Break
Vous pouvez parfois Annuler d’autres itérations. Vous pouvez le faire en incluant une instruction if instruction afin qu’aucune autre instruction dans le code n’est exécutées. Toutefois, vous pouvez également utiliser yield break pour provoquer MoveNext à retourner false et le contrôle retourne immédiatement à l’appelant et de fin de la boucle. Voici un exemple d’une telle méthode :
public System.Collections.Generic.IEnumerable<T>
GetNotNullEnumerator()
{
if((First == null) || (Second == null))
{
yield break;
}
yield return Second;
yield return First;
}
Cette méthode annule l’itération si un des éléments dans la classe de la paire < T > est null.
Une instruction yield break est similaire à placer une instruction de retour en haut d’une fonction lorsqu’il s’avère il n’existe aucune tâche à effectuer. Il s’agit d’un moyen pour quitter le programme à partir d’autres itérations sans le code de tous les autres if bloc. Par conséquent, elle permet à plusieurs sorties. Utilisez-le avec précaution, car une lecture informelle du code peut ignorer la sortie anticipée.
Fonctionnement des itérateurs
Lorsque le compilateur c# rencontre un itérateur, elle développe le code dans le CIL approprié pour le modèle de conception énumérateur correspondant. Dans le code généré, le compilateur c# crée d’abord une classe privée imbriquée pour implémenter l’interface IEnumerator < T >, ainsi que sa propriété actuelle et une méthode MoveNext. La propriété actuelle retourne un type correspondant au type de retour de l’itérateur. Comme vous l’avez vu dans Figure 3, paire < T > contient un itérateur qui retourne un type T. Le compilateur c# examine le code contenu dans l’itérateur et crée le code nécessaire au sein de la méthode MoveNext et la propriété actuelle pour simuler son comportement. Pour l’itérateur de la paire < T >, le compilateur c# génère du code à peu près équivalente (voir Figure 6).
Figure 6 C# équivalent Code généré par le compilateur c# pour les itérateurs
using System;
using System.Collections.Generic;
public class Pair<T> : IPair<T>, IEnumerable<T>
{
// ...
// The iterator is expanded into the following
// code by the compiler.
public virtual IEnumerator<T> GetEnumerator()
{
__ListEnumerator result = new __ListEnumerator(0);
result._Pair = this;
return result;
}
public virtual System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return new GetEnumerator();
}
private sealed class __ListEnumerator<T> : IEnumerator<T>
{
public __ListEnumerator(int itemCount)
{
_ItemCount = itemCount;
}
Pair<T> _Pair;
T _Current;
int _ItemCount;
public object Current
{
get
{
return _Current;
}
}
public bool MoveNext()
{
switch (_ItemCount)
{
case 0:
_Current = _Pair.First;
_ItemCount++;
return true;
case 1:
_Current = _Pair.Second;
_ItemCount++;
return true;
default:
return false;
}
}
}
}
Étant donné que le compilateur prend le rendement return (instruction) et génère des classes qui correspondent à ce que vous probablement serez avez écrit manuellement, itérateurs dans c# présentent les mêmes caractéristiques de performances en tant que classes qui implémentent le modèle de conception énumérateur manuellement. Bien qu’il n’existe aucune amélioration des performances, les gains de productivité de programmeur sont significatifs.
Création de plusieurs itérateurs dans une classe unique
Les exemples précédents itérateur implémenté IEnumerable < T >. GetEnumerator, qui est la méthode foreach recherches implicitement. Vous pouvez parfois séquences itération différente, comme une itération dans l’ordre inverse, le filtrage des résultats ou itération sur une projection de l’objet que celle par défaut. Vous pouvez déclarer des itérateurs supplémentaires dans la classe en les encapsulant dans Propriétés ou des méthodes qui retournent IEnumerable < T > ou IEnumerable. Si vous souhaitez effectuer une itération sur les éléments de la paire < T > dans l’ordre inverse, par exemple, vous pourriez fournir une méthode GetReverseEnumerator, comme indiqué dans Figure 7.
Figure 7 Utilisation Yield Return dans une méthode qui renvoie IEnumerable < T >
public struct Pair<T>: IEnumerable<T>
{
...
public IEnumerable<T> GetReverseEnumerator()
{
yield return Second;
yield return First;
}
...
}
public void Main()
{
var game = new Pair<string>("Redskins", "Eagles");
foreach (string name in game.GetReverseEnumerator())
{
Console.WriteLine(name);
}
}
Notez que vous retournez IEnumerable < T >, pas IEnumerator < T >. Cela est différent de IEnumerable < T >. GetEnumerator, qui renvoie IEnumerator < T >. Le code dans Main montre comment appeler GetReverseEnumerator à l’aide d’une boucle foreach.
Spécifications d’instruction yield
Vous pouvez utiliser l’instruction yield return uniquement dans les membres de type de retour IEnumerator < T > ou IEnumerable < T > ou leurs équivalents non génériques. Membres dont les corps incluent un rendement retournent l’instruction ne peut pas avoir un retour simple. Si le membre utilise l’instruction yield return, le compilateur c# génère le code nécessaire pour maintenir l’état de l’itérateur. En revanche, si le membre utilise l’instruction return au lieu de rendement de retour, le programmeur est chargé de gérer son propre ordinateur d’état et de retourner une instance de l’une des interfaces d’itérateur. En outre, comme tous les chemins de code dans une méthode avec un type de retour doivent contenir une instruction return accompagnée d’une valeur (en supposant qu’ils ne lèvent une exception), tous les chemins de code d’un itérateur doivent contenir une instruction yield return pour retourner des données.
Les restrictions supplémentaires suivantes sur l’instruction yield génèrent des erreurs du compilateur si elles sont violées :
- L’instruction yield peut apparaître qu’à l’intérieur d’une méthode, un opérateur défini par l’utilisateur ou l’accesseur get d’une propriété ou un indexeur. Le membre ne doit pas prendre ref ou paramètre du délai d’attente.
- L’instruction yield ne peut pas apparaître n’importe où à l’intérieur d’une expression lambda ou de méthode anonyme.
- L’instruction yield ne peut pas apparaître dans les clauses de l’instruction try catch et finally. En outre, une instruction yield peut apparaître dans un bloc try uniquement s’il n’existe aucun bloc catch.
Pour résumer
Interrogées génériques a été la fonctionnalité froid lancée en c# 2.0, mais il n’était pas la seule fonctionnalité liés à la collection introduite dans le temps. Un autre plus significatif a été l’itérateur. Comme mentionné dans cet article, les itérateurs impliquent un mot clé contextuel, taux de rendement que c# utilise pour générer le code CIL sous-jacente qui implémente le modèle d’itérateur utilisé par la boucle foreach. En outre, je détaillées la syntaxe de rendement, expliquant comment il répond à l’implémentation de GetEnumerator de IEnumerable < T >, permettant de sortir d’une boucle avec yield break et même prend en charge une méthode c# qui retourne un IEnumeable < T >.
Une grande partie de cette colonne dérive mon livre « essentielles c# » (IntelliTect.com/EssentialCSharp), laquelle je suis actuellement au beau milieu de la mise à jour vers « Essentielles c# 7.0. » Pour plus d’informations sur cette rubrique, consultez le chapitre 16.
Mark Michaelisest le créateur de IntelliTect, où il sert de son architecte technique principal et le formateur. Pour près de vingt, il a été Microsoft MVP 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 intervient lors de conférences et a écrit de nombreux ouvrages, y compris sa plus récente, « essentielles c# 6.0 (5ème édition) » (itl.tc/EssentialCSharp). Contactez-le sur Facebook à facebook.com/Mark.Michaelis, sur son blog à IntelliTect.com/Mark, sur Twitter : @markmichaelis ou par courrier électronique à mark@IntelliTect.com.
Grâce aux IntelliTect experts techniques suivants pour la révision de cet article : Kevin Bost