7 votes

List<T>.AddRange et l'instruction yield

Je suis conscient que le mot-clé yield indique que la méthode dans laquelle il apparaît est un itérateur. Je me demandais simplement comment cela fonctionne avec quelque chose comme List<T>.AddRange .

Prenons l'exemple ci-dessous :

static void Main()
{
    foreach (int i in MyInts())
    {
        Console.Write(i);
    }
}

public static IEnumerable<int> MyInts()
{  
    for (int i = 0; i < 255; i++)
    {
        yield return i;
    }
}

Ainsi, dans l'exemple ci-dessus, après chaque rendement, une valeur est renvoyée dans le champ foreach boucle dans Main et est imprimé sur la console.

Si nous changeons Main à ça :

static void Main()
{
    var myList = new List<int>();
    myList.AddRange(MyInts());
}

Comment cela fonctionne-t-il ? Est-ce que AddRange est appelé pour chaque int retourné par l'instruction yield ou attend-il les 255 valeurs avant d'ajouter la plage entière ?

4voto

Vache Points 13219

El mise en œuvre de AddRange itérera sur les IEnumerable en utilisant la fonction .MoveNext() jusqu'à ce que toutes les valeurs aient été produites par votre yield méthode de travail. On peut le constater aquí .

Así que myList.AddRange(MyInts()); n'est appelé qu'une fois et sa mise en œuvre force MyInts pour rendre toutes ses valeurs avant de continuer.

AddRange épuise toutes les valeurs de l'itérateur en raison de la manière dont elle est mise en œuvre, mais la méthode hypothétique suivante n'évaluerait que la première valeur de l'itérateur :

public void AddFirst<T>(IEnumerable<T> collection)
{
    Insert(collection.First());
}

Une expérience intéressante pendant que vous jouez avec cela est d'ajouter un Console.WriteLine(i); dans votre MyInts pour voir quand chaque numéro est généré.

0voto

Ray Fischer Points 498

Question intéressante.

Le comportement est différent si l'énumérable est pour une classe qui implémente ICollection, comme une autre liste ou un tableau, mais disons que ce n'est pas le cas puisque votre exemple ne le fait pas. AddRange() utilise simplement l'énumérateur pour insérer les éléments dans la liste, un par un.

            using(IEnumerator<T> en = collection.GetEnumerator()) {
                while(en.MoveNext()) {
                    Insert(index++, en.Current);  

Si le type de l'énumérateur est ICollection, AddRange développe d'abord la liste, puis effectue une copie en bloc.

Si vous voulez voir le code vous-même : https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,51decd510e5bfe6e

0voto

Réponse courte : Lorsque vous appelez AddRange il va itérer en interne chaque élément de votre IEnumerable et l'ajouter à la liste.

Si tu faisais quelque chose comme ça :

var myList = new List<int>();
myList.AddRange(MyInts());

foreach (int i in myList)
{
    Console.Write(i);
}

Vos valeurs seraient alors itérées deux fois, du début à la fin :

  • Une fois lors de l'ajout à votre liste
  • Alors dans votre for boucle

Jouer un peu

Maintenant, supposons que vous ayez créé votre propre méthode d'extension pour AddRange comme ça :

public static IEnumerable<T> AddRangeLazily<T>(this ICollection<T> col, IEnumerable<T> values)
{
    foreach (T i in values)
    {
        yield return i; // first we yield
        col.Add(i); // then we add
    }
}

Alors vous pourriez l'utiliser comme ceci :

foreach (int i in myList.AddRangeLazily(MyInts()))
{
    Console.Write(i);
}

...et il serait itéré deux fois également, sans aller du début à la fin les deux fois. Il ajouterait paresseusement chaque valeur à la liste/collection et vous permettrait en même temps de faire autre chose (comme l'imprimer sur la sortie) après chaque nouvel élément ajouté.

Si vous aviez une sorte de logique pour arrêter les en ajoutant à la liste au milieu de l'opération, cela devrait être utile en quelque sorte.

L'inconvénient, c'est que AddRangeLazily c'est-à-dire que les valeurs ne seront ajoutées à la collection qu'une fois que vous aurez itéré sur les éléments suivants AddRangeLazily comme mon exemple de code. Si tu fais juste ça :

var someList = new List<int>();
someList.AddRangeLazily(MyInts());
if (someList.Any())
    // it wouldn't enter here...

...il n'ajoutera pas de valeurs du tout. Si vous voulez ce comportement, vous devez utiliser AddRange . Forcer l'itérationg sur AddRangeLazily Mais la méthode de l'autre côté fonctionnerait :

var someList = new List<int>();
someList.AddRangeLazily(MyInts());
if (someList.AddRangeLazily(MyInts()).Count())
    // it would enter here...thus adding all values to the someList

...cependant, selon combien paresseux est la méthode que vous appelez, elle n'itérera pas tout. Par exemple :

var someList = new List<int>();
someList.AddRangeLazily(MyInts());
if (someList.AddRangeLazily(MyInts()).Any())
    // it would enter here, plus adding only the first value to someList

Depuis Any() est vrai dès que tout existe, alors Any() a juste besoin d'une itération pour retourner true Il n'a donc besoin que du premier élément pour être itéré.

Je ne me souviens pas avoir eu à faire quelque chose comme ça, c'était juste pour m'amuser. yield .

Fiddle ici ! !!

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X