Отложенное и немедленное выполнение LINQ
Есть два способа выполнения запроса LINQ: отложенное (deferred) и немедленное (immediate) выполнение.
При отложенном выполнении LINQ-выражение не выполняется, пока не будет произведена итерация или перебор по выборке, например, в цикле foreach. Обычно подобные операции возвращают объект IEnumerable<T> или IOrderedEnumerable<T>. Полный список отложенных операций LINQ:
AsEnumerable
Cast
Concat
DefaultIfEmpty
Distinct
Except
GroupBy
GroupJoin
Intersect
Join
OfType
OrderBy
OrderByDescending
Range
Repeat
Reverse
Select
SelectMany
Skip
SkipWhile
Take
TakeWhile
ThenBy
ThenByDescending
Union
Where
Рассмотрим отложенное выполнение:
string[] people = { "Tom", "Sam", "Bob" };
var selectedPeople = people.Where(s=>s.Length == 3).OrderBy(s=>s);
// выполнение LINQ-запроса
foreach (string s in selectedPeople)
Console.WriteLine(s);
То есть фактическое выполнение запроса происходит не в строке определения: var selectedPeople = people.Where..., а при переборе в цикле foreach.
Фактически LINQ-запрос разбивается на три этапа:
Получение источника данных
Создание запроса
Выполнение запроса и получение его результатов
Как это происходит в нашем случае:
Получение источника данных - определение массива teams:
string[] people = { "Tom", "Sam", "Bob" };
Создание запроса - определение переменной selectedTeams:
var selectedPeople = people.Where(s=>s.Length == 3).OrderBy(s=>s);
Выполнение запроса и получение его результатов:
foreach (string s in selectedPeople)
Console.WriteLine(s);
После определения запроса он может выполняться множество раз. И до выполнения запроса источник данных может изменяться. Чтобы более наглядно увидеть это, мы можем изменить какой-либо элемент до перебора выборки:
string[] people = { "Tom", "Sam", "Bob" };
var selectedPeople = people.Where(s=>s.Length == 3).OrderBy(s=>s);
people[2] = "Mike";
// выполнение LINQ-запроса
foreach (string s in selectedPeople)
Console.WriteLine(s);
Теперь выборка будет содержать два элемента, а не три, так как последний элемент после изменения не будет соответствовать условию.
Важно понимать, что переменная запроса сама по себе не выполняет никаких действий и не возвращает никаких данных. Она только хранит набор команд, которые необходимы для получения результатов. То есть выполнение запроса после его создания откладывается. Само получение результатов производится при переборе в цикле foreach.
Немедленное выполнение запроса
С помощью ряда методов мы можем применить немедленное выполнение запроса. Это методы, которые возвращают одно атомарное значение или один элемент или данные типов Array, List и Dictionary. Полный список подобных операций в LINQ:
Aggregate
All
Any
Average
Contains
Count
ElementAt
ElementAtOrDefault
Empty
First
FirstOrDefault
Last
LastOrDefault
LongCount
Max
Min
SequenceEqual
Single
SingleOrDefault
Sum
ToArray
ToDictionary
ToList
ToLookup
Рассмотрим пример с методом Count(), который возвращает число элементов последовательности:
string[] people = { "Tom", "Sam", "Bob" };
// определение и выполнение LINQ-запроса
var count = people.Where(s=>s.Length == 3).OrderBy(s=>s).Count();
Console.WriteLine(count); // 3 - до изменения коллекции
people[2] = "Mike";
Console.WriteLine(count); // 3 - после изменения коллекции
Результатом метода Count будет объект int, поэтому сработает немедленное выполнение.
Сначала создается запрос: people.Where(s=>s.Length == 3).OrderBy(s=>s). Далее к нему применяется метод Count(), который выполняет запрос, неявно выполняет перебор по последовательности элементов, генерируемой этим запросом, и возвращает число элементов в этой последовательности.
Также мы можем изменить код таким образом, чтобы метод Count() учитывал изменения и выполнялся отдельно от определения запроса:
string[] people = { "Tom", "Sam", "Bob" };
// определение LINQ-запроса
var selectedPeople = people.Where(s=>s.Length == 3).OrderBy(s=>s);
// выполнение запроса
Console.WriteLine(selectedPeople.Count()); // 3 - до изменения коллекции
people[2] = "Mike";
// выполнение запроса
Console.WriteLine(selectedPeople.Count()); // 2 - после изменения коллекции
Также для немедленного выполнения LINQ-запроса и кэширования его результатов мы можем применять методы преобразования ToArray<T>(), ToList<T>(), ToDictionary() и т.д.. Эти методы получают результат запроса в виде объектов Array, List и Dictionary соответственно. Например:
string[] people = { "Tom", "Sam", "Bob" };
// определение и выполнение LINQ-запроса
var selectedPeople = people.Where(s=>s.Length == 3).OrderBy(s=>s).ToList();
// изменение массива никак не затронет список selectedPeople
people[2] = "Mike";
// выполнение запроса
foreach (string s in selectedPeople)
Console.WriteLine(s);