Метод ToList в Linq

Если я не ошибаюсь, метод ToList() перебирает каждый элемент предоставленной коллекции и добавляет их в новый экземпляр List и возвращает этот экземпляр. Представьте пример

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

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

Я думаю, что традиционный способ выполняется быстрее, поскольку он циклически только один раз, где, как и выше Linq, повторяется дважды один раз для метода Where, а затем для метода ToList().

Проект, над которым я сейчас работаю, широко использует списки по всему миру, и я вижу, что есть много такого использования ToList() и других методов, которые могут быть сделаны лучше, чем выше, если я возьму list как IEnumerable и удалить .ToList() и использовать его далее как IEnumerable.

Помогают ли эти вещи влиять на производительность?

Ответ 1

Помогают ли эти вещи влиять на производительность?

Это зависит от вашего кода. В большинстве случаев использование LINQ приводит к небольшому результату. В некоторых случаях этот удар может быть значительным для вас, но вам следует избегать LINQ только тогда, когда вы знаете, что он слишком медленный для вас (т.е. Если профилирование вашего кода показало, что LINQ является причиной того, что ваш код медленный).

Но вы правы, что использование ToList() слишком часто может привести к серьезным проблемам с производительностью. Вы должны называть ToList() только тогда, когда вам нужно. Имейте в виду, что есть также случаи, когда добавление ToList() может значительно повысить производительность (например, когда коллекция загружается из базы данных при каждом ее повторе).

Относительно количества итераций: это зависит от того, что именно вы подразумеваете под "итерациями дважды". Если вы подсчитаете количество раз, когда MoveNext() вызывается в какой-либо коллекции, то да, используя Where(), этот способ приводит к повторному повторению. Последовательность операций выполняется так (для упрощения, я собираюсь предположить, что все элементы соответствуют условию):

  • Where() вызывается, итерация пока отсутствует, Where() возвращает специальный перечислимый. Вызывается
  • ToList(), вызывая MoveNext() в перечисляемом возврате из Where().
  • Where() теперь вызывает MoveNext() в исходной коллекции и получает значение.
  • Where() вызывает ваш предикат, который возвращает true.
  • MoveNext(), вызванный из ToList() возвращает, ToList() получает значение и добавляет его в список.
  • ...

Это означает, что если все n элементов в исходной коллекции соответствуют условию, MoveNext() будет называться 2 n раз, n раз из Where() и n раз из ToList().

Ответ 2

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

Это приведет к созданию запроса, а не к циклу элементов до тех пор, пока не будет использован запрос. Вызвав ToList(), сначала выполним запрос и, таким образом, только цикл ваших элементов один раз.

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

этот пример также будет повторяться только один раз. Потому что он используется только один раз. Имейте в виду, что список будет перебирать всех студентов каждый раз, когда он вызывается. Не только те, чьи имена ABC. Поскольку его запрос.

И для более позднего обсуждения Ive сделал тестовый пример. Возможно, это не самая лучшая реализация IEnumable, но она делает то, что она должна делать.

Сначала у нас есть наш список

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);
    }
}

И поскольку мы хотим подсчитать, сколько раз вызывается MoveNext, мы должны реализовать наш пользовательский счетчик aswel. Наблюдайте в MoveNext, у нас есть счетчик, который является статичным в нашей программе.

открытый класс TestEnumerator: IEnumerator   {       public Item FirstItem = null;       public Item 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);
    }
}

И тогда у нас есть пользовательский элемент, который просто обертывает наше значение

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

    public T Value;

    public Item<T> NextItem;
}

Чтобы использовать фактический код, мы создаем "список" с тремя записями.

    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.
    }

И заключение? Как это влияет на производительность? В этом случае MoveNext называется n + 1 раз. Независимо от того, сколько у нас предметов. А также WhereClause не имеет значения, он все равно будет запускать MoveNext 4 раза. Потому что мы всегда запускаем наш запрос в нашем первоначальном списке. Единственное достижение производительности, которое мы возьмем, - это реальная структура LINQ и ее вызовы. Фактические петли будут одинаковыми.

И прежде, чем кто-нибудь спросит, почему его N + 1 раз, а не N раз. Это потому, что он возвращает ложь в последний раз, когда у него нет элементов. Создание количества элементов + конец списка.

Ответ 3

Прежде всего, Почему вы даже спрашиваете меня? Измерьте для себя и посмотрите.

Тем не менее, Where, Select, OrderBy и другие методы расширения LINQ IEnumerable, в общем, реализованы как можно более ленивые (yield часто используется). Это означает, что они не работают с данными, если только они этого не делают. В вашем примере:

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

ничего не выполнит. Это мгновенно вернется, даже если Students - список из 10 миллионов объектов. Предикат не будет вызываться вообще, пока результат не будет фактически запрошен где-то, и это практически то, что делает ToList(): В нем говорится: "Да, результаты - все они - требуются немедленно".

Однако есть некоторые начальные накладные расходы при вызове методов LINQ, поэтому традиционный способ, в общем, будет более быстрым, но сложность и простота использования методов LINQ, IMHO, более чем компенсируют это.

Если вы хотите взглянуть на то, как эти методы реализованы, они доступны для справки из Источники ссылок Microsoft.

Ответ 4

Чтобы полностью ответить на этот вопрос, это зависит от реализации. Если вы говорите о LINQ to SQL/EF, в этом случае будет только одна итерация, когда вызывается .ToList, который внутренне вызывает .GetEnumerator. Затем выражение запроса анализируется в TSQL и передается в базу данных. Полученные строки затем повторяются (один раз) и добавляются в список.

В случае LINQ to Objects есть только один проход через данные. Использование возврата доходности в предложении where устанавливает внутреннюю машину состояния, которая отслеживает, где процесс находится на итерации. Где НЕ делает полную итерацию, создавая временный список, а затем передавая эти результаты остальной части запроса. Он просто определяет, соответствует ли элемент критериям и проходит только те, которые соответствуют.