2 votes

Pourquoi LINQ traite-t-il différemment deux méthodes qui font la "même" chose ?

Je suis tombé sur une question intéressante aujourd'hui : j'ai deux méthodes qui, à première vue, font la même chose. C'est-à-dire retourner un IEnumerable d'objets Foo.

Je les ai définies ci-dessous comme List1 et List2 :

public class Foo
{
    public int ID { get; set; }
    public bool Enabled { get; set;}    
}

public static class Data
{
    public static IEnumerable<Foo> List1
    {
        get
        {
            return new List<Foo>
            {
                new Foo {ID = 1, Enabled = true},
                new Foo {ID = 2, Enabled = true},
                new Foo {ID = 3, Enabled = true}
            };
        }
    }

    public static IEnumerable<Foo> List2
    {
        get
        {
            yield return new Foo {ID = 1, Enabled = true};
            yield return new Foo {ID = 2, Enabled = true};
            yield return new Foo {ID = 3, Enabled = true};
        }
    }
}

Considérons maintenant les tests suivants :

IEnumerable<Foo> listOne = Data.List1;
listOne.Where(item => item.ID.Equals(2)).First().Enabled = false;
Assert.AreEqual(false, listOne.ElementAt(1).Enabled);
Assert.AreEqual(false, listOne.ToList()[1].Enabled);  

IEnumerable<Foo> listTwo = Data.List2;
listTwo.Where(item => item.ID.Equals(2)).First().Enabled = false;
Assert.AreEqual(false, listTwo.ElementAt(1).Enabled);
Assert.AreEqual(false, listTwo.ToList()[1].Enabled);  

Ces deux méthodes semblent faire la même chose.

Pourquoi les deuxièmes assertions du code de test échouent-elles ?
Pourquoi le deuxième élément "Foo" de listTwo n'est-il pas mis à false alors qu'il se trouve dans listOne ?

NOTE : Je cherche à savoir pourquoi cela est autorisé et quelles sont les différences entre les deux. Je ne cherche pas à savoir comment corriger la seconde assertion, car je sais que si j'ajoute un appel ToList à List2, cela fonctionnera.

6voto

Jonathan Rupp Points 10900

Le premier bloc de code construit les éléments une seule fois et renvoie une liste contenant les éléments.

Le deuxième bloc de code construit ces éléments à chaque fois que l'IEnumerable est parcouru.

Cela signifie que la deuxième et la troisième ligne du premier bloc agissent sur la même instance d'objet. Les deuxième et troisième lignes du deuxième bloc agissent sur différents instances de Foo (de nouvelles instances sont créées au fur et à mesure de l'itération).

La meilleure façon de le constater est de placer des points d'arrêt dans les méthodes et d'exécuter ce code avec le débogueur. La première version n'atteindra le point d'arrêt qu'une seule fois. La deuxième version l'atteindra deux fois, une fois pendant l'appel à .Where(), et une fois pendant l'appel à .ElementAt. (edit : avec le code modifié, le point d'arrêt sera également atteint une troisième fois, lors de l'appel à ToList()).

Ce qu'il faut retenir ici, c'est qu'une méthode itératrice (c'est-à-dire qui utilise le retour de rendement) sera exécutée tous chaque fois que l'énumérateur est parcouru, et pas seulement lorsque la valeur de retour initiale est construite.

2voto

Joel Coehoorn Points 190579

Il s'agit bien de no la même chose.

La première construit et renvoie une liste au moment où vous l'appelez, et vous pouvez la convertir en liste et faire des choses avec si vous le souhaitez, y compris ajouter ou supprimer des éléments, et une fois que vous avez mis les résultats dans une variable, vous agissez sur cet ensemble unique de résultats. Appeler la fonction produirait un autre ensemble de résultats, mais réutiliser le résultat d'un seul appel agit sur les mêmes objets.

Le second construit un IEnumerable. Vous pouvez l'énumérer, mais vous ne pouvez pas le traiter comme une liste sans appeler d'abord .ToList() sur celui-ci. En fait, l'appel de la méthode ne fait pas n'importe quoi jusqu'à ce que vous l'itériez réellement. Envisagez de le faire :

var fooList = Data.List2().Where(f => f.ID > 1);
// NO foo objects have been created yet.
foreach (var foo in fooList)
{
   // a new Foo object is created, but NOT until it's actually used here
   Console.WriteLine(foo.Enabled.ToString());  
}

Notez que le code ci-dessus créera la première instance Foo (non utilisée), mais pas avant d'entrer dans la boucle foreach. Les éléments ne sont donc pas réellement créés avant d'être appelés. Mais cela signifie qu'à chaque fois que vous les appelez, vous créez un nouvel ensemble d'éléments.

1voto

Jeff Meatball Yang Points 12021

ListTwo est un itérateur - une machine à états.

ElementAt doit commencer au début de l'itérateur pour obtenir correctement le i-ième indice dans l'IEnumerable (qu'il s'agisse ou non d'une machine d'état itératrice ou d'une véritable instance d'IEnumerable), et à ce titre, listTwo sera réinitialisée avec les valeurs par défaut de Enabled = true pour les trois éléments.

0voto

Shafqat Ahmed Points 1103

Suggestion : Compilez le code et ouvrez-le avec reflector. Yield est une suggestion syntaxique. Vous pourriez voir la différence de logique entre le code que vous avez écrit et le code généré pour le mot-clé yield. Les deux ne sont pas identiques.

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