web-dev-qa-db-fra.com

Méthode ToList dans Linq

Si je ne me trompe pas, la méthode ToList () itère sur chaque élément de la collection fournie et les ajoute à la nouvelle instance de List et renvoie cette instance. Supposons un exemple

//using linq
list = Students.Where(s => s.Name == "ABC").ToList();

//traditional way
foreach (var student in Students)
{
  if (student.Name == "ABC")
    list.Add(student);
}

Je pense que la méthode traditionnelle est plus rapide, car elle ne boucle qu'une seule fois, alors qu'à partir de Linq, elle itère deux fois pour la méthode Where et ensuite pour la méthode ToList ().

Le projet sur lequel je travaille a actuellement une utilisation intensive des listes et je constate qu’il existe beaucoup d’utilisation de ce type d’utilisation de ToList () et d’autres méthodes qui peuvent être améliorées comme ci-dessus si je prends list variable as IEnumerable et supprimez .ToList () et utilisez-le ensuite en tant que IEnumerable.

Ces choses ont-elles un impact sur les performances?

12
user1976469

Ces choses ont-elles un impact sur les performances?

Cela dépend de votre code. La plupart du temps, l'utilisation de LINQ entraîne peu de problèmes de performances. Dans certains cas, cet impact peut être significatif pour vous, mais vous ne devez éviter LINQ que si vous savez qu'il est trop lent (c'est-à-dire si le profilage de votre code indique que LINQ est la raison pour laquelle votre code est lent).

Mais vous avez raison, le fait d'utiliser trop souvent ToList() peut entraîner d'importants problèmes de performances. Vous ne devez appeler ToList() que lorsque vous devez le faire. Sachez qu'il existe également des cas où l'ajout de ToList() peut considérablement améliorer les performances (par exemple, lorsque la collection est chargée à partir de la base de données chaque fois qu'elle est itérée).

En ce qui concerne le nombre d'itérations: cela dépend de ce que vous entendez exactement par «itérer deux fois». Si vous comptez le nombre d'appels de MoveNext() sur une collection, alors oui, utiliser Where() de cette façon conduit à une itération deux fois. La séquence des opérations est la suivante (pour simplifier, je vais supposer que tous les éléments correspondent à la condition):

  1. Where() est appelé, pas d'itération pour l'instant, Where() renvoie un énumérable spécial.
  2. ToList() est appelé, appelant MoveNext() sur l'énumérable renvoyé par Where().
  3. Where() appelle maintenant MoveNext() sur la collection d'origine et obtient la valeur.
  4. Where() appelle votre prédicat, qui renvoie true.
  5. MoveNext() appelé depuis ToList() renvoie, ToList() récupère la valeur et l'ajoute à la liste.

Cela signifie que si tous les éléments n de la collection d'origine correspondent à la condition, MoveNext() sera appelé 2n fois, n fois de Where() et n fois de ToList().

9
svick
var list = Students.Where(s=>s.Name == "ABC"); 

Cela ne fera que créer une requête et non boucler les éléments jusqu'à ce que la requête soit utilisée. En appelant ToList (), vous exécuterez d'abord la requête et ne bouclerez donc qu'une fois vos éléments.

List<Student> studentList = new List<Student>();
var list = Students.Where(s=>s.Name == "ABC");
foreach(Student s in list)
{
    studentList.add(s);
}

cet exemple ne sera également itérer qu'une fois. Parce que ce n'est utilisé qu'une fois. N'oubliez pas que cette liste itérera tous les étudiants à chaque fois qu'elle s'appelle .. Pas seulement ceux dont le nom est ABC. Depuis c'est une requête.

Et pour la discussion ultérieure, j'ai fait un exemple de test. Ce n’est peut-être pas la meilleure implémentation d’Ienumable mais il fait ce qu’il est supposé faire.

Nous avons d'abord notre liste

public class TestList<T> : IEnumerable<T>
{
    private TestEnumerator<T> _Enumerator;

    public TestList()
    {
        _Enumerator = new TestEnumerator<T>();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _Enumerator;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }

    internal void Add(T p)
    {
        _Enumerator.Add(p);
    }
}

Et puisque nous voulons compter le nombre de fois que MoveNext est appelé, nous devons implémenter notre énumérateur personnalisé. Observez dans MoveNext nous avons un compteur qui est statique dans notre programme.

classe publique TestEnumerator: IEnumerator {élément public FirstItem = null; élément public CurrentItem = null;

    public TestEnumerator()
    {
    }

    public T Current
    {
        get { return CurrentItem.Value; }
    }

    public void Dispose()
    {

    }

    object System.Collections.IEnumerator.Current
    {
        get { throw new NotImplementedException(); }
    }

    public bool MoveNext()
    {
        Program.Counter++;
        if (CurrentItem == null)
        {
            CurrentItem = FirstItem;
            return true;
        }
        if (CurrentItem != null && CurrentItem.NextItem != null)
        {
            CurrentItem = CurrentItem.NextItem;
            return true;
        }
        return false;
    }

    public void Reset()
    {
        CurrentItem = null;
    }

    internal void Add(T p)
    {
        if (FirstItem == null)
        {
            FirstItem = new Item<T>(p);
            return;
        }
        Item<T> lastItem = FirstItem;
        while (lastItem.NextItem != null)
        {
            lastItem = lastItem.NextItem;
        }
        lastItem.NextItem = new Item<T>(p);
    }
}

Et puis nous avons un article personnalisé qui enveloppe notre valeur

public class Item<T>
{
    public Item(T item)
    {
        Value = item;
    }

    public T Value;

    public Item<T> NextItem;
}

Pour utiliser le code actuel, nous créons une "liste" avec 3 entrées.

    public static int Counter = 0;
    static void Main(string[] args)
    {
        TestList<int> list = new TestList<int>();
        list.Add(1);
        list.Add(2);
        list.Add(3);

        var v = list.Where(c => c == 2).ToList(); //will use movenext 4 times
        var v = list.Where(c => true).ToList();   //will also use movenext 4 times


        List<int> tmpList = new List<int>(); //And the loop in OP question
        foreach(var i in list)
        {
            tmpList.Add(i);
        }                                    //Also 4 times.
    }

Et conclusion? Comment ça frappe la performance? Le MoveNext s'appelle n + 1 fois dans ce cas. Peu importe le nombre d'articles que nous avons. Et aussi la WhereClause n’a pas d’importance, il exécutera quand même MoveNext 4 fois. Parce que nous posons toujours notre requête sur notre liste initiale. Le seul impact sur les performances que nous aurons à subir est le framework LINQ et ses appels. Les boucles réelles seront les mêmes.

Et avant que quiconque ne demande pourquoi c'est N + 1 fois et non pas N fois. C'est parce qu'il retourne faux la dernière fois qu'il n'a plus d'éléments. Ce qui en fait le nombre d'éléments + fin de liste.

4
Evelie

Pour répondre complètement à cette question, cela dépend de la mise en œuvre. Si vous parlez de LINQ to SQL/EF, il n'y aura qu'une seule itération dans ce cas lorsque .ToList est appelé, qui appelle en interne .GetEnumerator. L'expression de requête est ensuite analysée dans TSQL et transmise à la base de données. Les lignes résultantes sont ensuite itérées (une fois) et ajoutées à la liste.

Dans le cas de LINQ to Objects, un seul passage est également effectué dans les données. L’utilisation de return return dans la clause where configure une machine à états en interne qui garde la trace du processus dans l’itération. Where NE fait PAS une itération complète en créant une liste temporaire, puis en transmettant ces résultats au reste de la requête. Il détermine simplement si un élément répond à un critère et ne transmet que ceux qui correspondent.

1
Jim Wooley

Tout d'abord, Pourquoi tu me demandes même? Mesurez vous-même et voyez.

Cela dit, Where, Select, OrderBy et les autres méthodes d’extension LINQ IEnumerable, en général, sont implémentées le plus paresseux possible (le mot clé yield est souvent utilisé). Cela signifie qu'ils ne travaillent pas sur les données à moins d'y être obligés. De votre exemple:

var list = Students.Where(s => s.Name == "ABC");

n'exécutera rien. Cela reviendra momentanément même si Students est une liste de 10 millions d'objets. Le prédicat ne sera pas appelé du tout jusqu'à ce que le résultat soit réellement demandé quelque part, et c'est pratiquement ce que fait ToList(): il dit "Oui, les résultats - tous - sont requis immédiatement".

L'appel des méthodes LINQ présente toutefois un surcoût initial, si bien que la méthode traditionnelle sera en général plus rapide, mais la composabilité et la facilité d'utilisation des méthodes LINQ, IMHO, compenseront largement ces inconvénients.

Si vous souhaitez examiner la manière dont ces méthodes sont implémentées, elles peuvent être consultées à partir de Microsoft Reference Sources .

1
SWeko