Содержание
LINQ — Language Integrated Query (Внутриязыковой запрос) — технология, представляющая собой набор функций, позволяющих писать структурированные типобезопасные запросы к локальным объектам-коллекциям и удаленным источникам данных.
С помощью LINQ можно писать запросы к любой коллекции, реализующей интерфейсIEnumerable<>, например, к массивам, спискам (list), XML DOM, удаленным источникам данных, таким как таблицы SQL сервера. LINQ объединяет преимущества динамических запросов и проверки типов при компиляции.
Основы LINQ
Базовыми единицами данных в LINQ являются последовательности и элементы. Последовательность — любой объект, реализующий обобщенный интерфейс IEnumerable, элемент — каждая единица последовательности. В следующем примере names — последовательность, а Tom, Dick, и Harry — элементы:
| 1 | string[] names = { "Tom", "Dick", "Harry" }; | 
Последовательности как эта называются локальными, т.к. представляют собой локальные коллекции объектов, расположенные в памяти.
Оператор запроса (query operator) — метод, преобразующий последовательность. Обычно операторы запроса принимают входящую последовательность и возвращают преобразованную исходящую последовательность. Класс Enumerable в пространстве имен System.Linq содержит около 40 операторов запроса; все они являются статическими методами расширениями (extension methods). Эти 40 операторов называютсястандартными операторами запроса.
LINQ также поддерживает последовательности, которые могут быть динамически извлечены из удаленных источников данных, таких как SQL сервер. Эти последовательности дополнительно реализуют интерфейс IQueryable<>, а указанные стандартные операторы запроса для них определяются в классе Queryable.
Простой запрос, оператор Where
Запрос представляет собой выражение, которое преобразует последовательности с помощью одного или нескольких операторов запроса. Простейший запрос включает одну входящую последовательность и один оператор запроса. Например, с помощью оператора Where можно извлечь из простого массива все имена, чья длина как минимум 4 символа:
| 1 2 3 4 5 | string[] names = { "Tom", "Dick", "Harry" }; IEnumerable<string> filteredNames =     System.Linq.Enumerable.Where(names, n => n.Length >= 4); foreach (string n in filteredNames)     Console.Write (n + "|"); // Dick|Harry| | 
 Т.к. стандартные операторы запроса реализованы как методы расширения, мы можем вызвать Where непосредственно у массива names так, как если бы он был экземплярным методом:
| 1 2 3 4 | using System.Linq ... IEnumerable<string> filteredNames =     names.Where (n => n.Length >= 4); | 
 В последнем примере необходимо импортировать пространство имен System.Linq с помощью директивы using иначе код не скомпилируется.
Метод Where класса System.Linq.Enumerable имеет следующую сигнатуру:
| 1 2 3 | static IEnumerable<TSource> Where<TSource> (     this IEnumerable<TSource> source,     Func<TSource,bool> predicate) | 
 Здесь source — входящая последовательность, а predicate — делегат, который вызывается для каждого его элемента. Метод Where в исходящую последовательность включит все элементы, для которых делегат вернет true. Внутренне он реализуется с помощью итератора:
| 1 2 3 | foreach (TSource element in source)     if (predicate (element))         yield return element; | 
Проецирование (Projecting), оператор Select
Другой основополагающий оператор запроса — метод Select. Он преобразует (проецирует) каждый элемент входящей последовательности с помощью лямбда выражения:
| 1 2 3 4 5 | string[] names = { "Tom", "Dick", "Harry" }; IEnumerable<string> upperNames =     names.Select (n => n.ToUpper()); foreach (string n in upperNames)     Console.Write (n + "|"); // TOM|DICK|HARRY| | 
Запрос может проецировать анонимный тип:
| 1 2 3 4 5 6 7 8 9 10 11 | var query = names.Select (         n => new {                   Name = n,                   Length = n.Length }); foreach (var row in query)     Console.WriteLine (row); // Результат: // { Name = Tom, Length = 3 } // { Name = Dick, Length = 4 } // { Name = Harry, Length = 5 } | 
Операторы Take и Skip
Исходная очередность элементов в входящей последовательности в LINQ имеет существенное значение. Некоторые операторы запроса, такие как Take,Skip и Reverse, способны оказывать влияние на эту очередность.
Оператор Take возвращает в исходящей последовательности первые n элементов, отбрасывая остальные:
| 1 2 3 | int[] numbers = { 10, 9, 8, 7, 6 }; IEnumerable<int> firstThree = numbers.Take (3); // вернет { 10, 9, 8 } | 
 Оператор Skip, напротив, отбрасывает первые n элементов и возвращает в исходящей последовательности оставшиеся:
| 1 | IEnumerable<int> lastTwo = numbers.Skip (3); | 
Элементные операторы (Element operators)
Не все операторы запроса возвращают последовательность. Элементные операторы, например: First, Last, Single и ElementAt, извлекают один элемент из входящей последовательности.
| 1 2 3 4 5 | int[] numbers = { 10, 9, 8, 7, 6 }; int firstNumber = numbers.First(); // 10 int lastNumber = numbers.Last(); // 6 int secondNumber = numbers.ElementAt (2); // 8 int firstOddNum = numbers.First (n => n%2 == 1); // 9 | 
 Все эти операторы выбрасывают исключение, если последовательность не содержит подходящего элемента. Чтобы вместо исключения получить null (пустой результат), нужно использовать соответственно операторы FirstOrDefault, LastOrDefault,SingleOrDefault или ElementAtOrDefault.
Операторы Single и SingleOrDefault эквивалентны операторам Firstи FirstOrDefault, за исключением того, что они выбрасывают исключение если подходящих элементов больше одного.
Агрегирующие операторы (Aggregation operators)
Агрегирующие операторы возвращают скалярное значение как правило числового типа. Наиболее часто используются агрегирующие операторы Count, Min, Max и Average.
| 1 2 3 4 5 | int[] numbers = { 10, 9, 8, 7, 6 }; int count = numbers.Count(); // 5 int min = numbers.Min(); // 6 int max = numbers.Max(); // 10 double avg = numbers.Average(); // 8 | 
 Оператор Count подсчитывает количество элементов в последовательности. Ему можно передать факультативный предикат, тогда оператор посчитает только те элементы, для которых предикат вернет true:
| 1 | int maxRemainderAfterDivBy5 = numbers.Max (n => n % 5); // 4 | 
 Операторы Min, Max и Average также могут принимать факультативный аргумент, который преобразует каждый элемент последовательности перед тем, как выполнить агрегацию:
| 1 2 | int maxRemainderAfterDivBy5 = numbers.Max (n => n % 5); // 4 double rms = Math.Sqrt (numbers.Average (n => n * n)); | 
Кванторы (Quantifiers)
Квантификацией в логике (от лат. quantum — сколько) называется задание пределов, определяющих количество объектов, для которых применимо выражение. Широко применяются два квантора: существования (имеется такой) и всеобщности (для всех, для любого).
Операторы кванторы в LINQ возвращают логическое (bool) значение. К ним относятся Contains, Any, All, and SequenceEquals (последний сравнивает две последовательности):
| 1 2 3 4 5 | int[] numbers = { 10, 9, 8, 7, 6 }; bool hasTheNumberNine = numbers.Contains (9); // true bool hasMoreThanZeroElements = numbers.Any(); // true bool hasOddNum = numbers.Any (n => n % 2 == 1); // true bool allOddNums = numbers.All (n => n % 2 == 1); // false | 
Операторы комплектовщики (Set operators)
Операторы комплектовщики принимают две входящие последовательности одного типа. Оператор Concat присоединяет одну последовательность к другой. Оператор Unionделает тоже самое, но при этом удаляет повторы.
| 1 2 3 4 | int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 }; IEnumerable<int>     concat = seq1.Concat (seq2), // { 1, 2, 3, 3, 4, 5 }     union = seq1.Union (seq2), // { 1, 2, 3, 4, 5 } | 
 Также к этой категории относятся операторы Intersect и Except:
| 1 2 3 4 | IEnumerable<int>     commonality = seq1.Intersect (seq2), // { 3 }     difference1 = seq1.Except (seq2), // { 1, 2 }     difference2 = seq2.Except (seq1); // { 4, 5 } | 
Отложенное выполнение (Deferred Execution)
Важной особенностью многих операторов запроса является то, что они выполняются не в момент их создания, а в момент перечисления исходящей последовательности (т.е. когда вызывается метод MoveNext их нумератора):
| 1 2 3 4 5 6 | var numbers = new List<int> { 1 }; numbers.Add (1); IEnumerable<int> query = numbers.Select (n => n * 10); numbers.Add (2); // Вставляем еще один элемент foreach (int n in query)     Console.Write (n + "|"); // 10|20| | 
 В этом примере дополнительное число, которое мы вставили в список после создания запроса также было включено в результат, поскольку результат был сформирован только во время выполнения инструкции foreach. Эта особенность получила названиеотложенное (deferred) или ленивое (lazy) выполнение. Отложенное выполнение отделяет создание запросов от их выполнения, позволяя создавать запросы в несколько шагов. Все стандартные операторы запроса предполагают отложенное выполнение, за следующим исключением:
- операторы, возвращающие отдельные элементы или скалярные значения (элементные операторы (element operators), операторы объединения (aggregation operators) и операторы кванторы (quantifiers))
- следующие операторы преобразования (conversion operators): ToArray,ToList,ToDictionary,ToLookup
Операторы преобразования могут быть очень удобны как раз благодаря тому, что они устраняют отложенное выполнение. С их помощью можно словно заморозить или кэшировать результат в определенный момент выполнения программы, чтобы избежать повторного выполнения сложных запросов, требующих большого количества машинных ресурсов, а также запросов к удаленным источникам данных.
| 1 2 3 4 5 6 | var numbers = new List<int>() { 1, 2 }; List<int> timesTen = numbers     .Select (n => n * 10)     .ToList(); // Выполняется немедленно numbers.Clear(); Console.WriteLine (timesTen.Count); // Все еще 2 | 
Вложенные запросы (subqueries) позволяют обойти указанные выше исключения из отложенного выполнения. Все вложенные запросы выполняются в момент выполнения основного запроса. Поэтому если основной запрос выполняется отложено, то и все его вложенные запросы, в том числе объединяющие и преобразующие операторы, выполняются отложено:
| 1 2 3 | names.Where (     n => n.Length ==         names.Min (n2 => n2.Length)) | 
Список стандартных операторов запроса
Стандартные операторы запроса, реализованные в классе System.Linq.Enumerable, можно разделить на 12 категорий.
Фильтрующие операторы (Filtering operators)
Фильтрующие операторы возвращают подмножество элементов, удовлетворяющих определенному условию:
- Where— возвращает подмножество элементов, удовлетворяющих переданному условию
- Take— возвращает первые n элементов, отбрасывая остальные
- Skip— отбрасывает первые n элементов и возвращает оставшиеся
- TakeWhile— извлекает элементы из входящей последовательности пока переданный предикат возвращает- true
- SkipWhile— отбрасывает элементы из входящей последовательности пока переданный предикат возвращает- true, затем возвращает остаток
- Distinct— возвращает коллекцию с исключенными повторами
Проецирующие операторы (Projection operators)
Проецирующие операторы преобразуют каждый элемент с помощью лямбда выражения, дополнительно расширяя подпоследовательности:
- Select— преобразует каждый элемент входящей последовательности с помощью переданного лямбда выражения
- SelectMany— преобразует элементы нескольких входящих последовательностей и объединяет получившиеся последовательности в одну (одноуровневую)
Объединяющие операторы (Joining operators)
Объединяющие операторы объединяют элементы одной последовательности с другой, используя эффективную технологию поиска:
- Join— объединяет элементы из двух последовательностей в один одноуровневый набор
- GroupJoin— объединяет элементы из двух последовательностей в один иерархический (многоуровневый) набор
- Zip— перебирает две последовательности за один проход и возвращает последовательность, содержащую результаты выполнения функции (переданной в качестве аргумента) над парами элементов из двух последовательностей
Упорядочивающие операторы (Ordering operators)
Упорядочивающие операторы возвращают переупорядоченную последовательность:
- OrderBy,- ThenBy— возвращают элементы отсортированные в возрастающем порядке
- OrderByDescending,- ThenByDescending— возвращают элементы отсортированный в убывающем порядке
- Reverse— возвращает элементы в обратном порядке
Группирующие операторы (Grouping operators)
Группирующие операторы группируют последовательность в подпоследовательности:
- GroupBy— группирует элементы последовательности в подмножества (подпоследовательности)
Операторы комплектовщики (Set operators)
Операторы комплектовщики принимают две последовательности одного типа и возвращают их общность, совокупность или разницу:
- Concat— объединяет две последовательности
- Union— объединяет две последовательности, удаляя повторы
- Intersect— возвращает элементы, присутствующие в обеих последовательностях
- Except— возвращает элементы первой последовательности, отсутствующие во второй
Элементные операторы (Element operators)
Элементные операторы выбирают отдельный элемент из последовательности:
- First,- FirstOrDefault— возвращают первый элемент последовательности или первый элемент, удовлетворяющий переданному предикату
- Last,- LastOrDefault— возвращают последний элемент последовательности или последний элемент, удовлетворяющий переданному предикату
- Single,- SingleOrDefault— эквивалент- First/- FirstOrDefault, но если совпадений больше одного, выбрасывает исключение
- ElementAt,- ElementAtOrDefault— возвращает элемент с указанной позицией
- DefaultIfEmpty— возвращает элементы последовательности илиодноэлементную коллекцию со значением по умолчанию — default(TSource), если последовательность пуста
Агрегирующие операторы (Aggregation operators)
Агрегирующие операторы выполняют вычисления над последовательностью и возвращают скалярное значение, обычно числового типа:
- Count,- LongCount— возвращает общее число элементов во входящей последовательности или число элементов, удовлетворяющих переданному предикату
- Min— возвращает наименьший элемент в последовательности
- Max— возвращает наибольший элемент в последовательности
- Sum— вычисляет сумму элементов в последовательности
- Average— вычисляет среднее значение элементов в последовательности
- Aggregate— выполняет пользовательскую агрегацию
Операторы кванторы (Quantifiers)
Операторы кванторы выполняют вычисления над последовательностью и возвращаютtrue или false:
- Contains— возвращает- trueесли входящая последовательность содержит переданный элемент
- Any— возвращает- trueесли хотя бы один элемент последовательности удовлетворяет переданному предикату
- All— возвращает- trueесли все элементы последовательности удовлетворяют переданному предикату
- SequenceEqual— возвращает- trueесли вторая (переданная в качестве аргумента) последовательность содержит элементы идентичные элементам входящей последовательности
Преобразующие-импортирующие операторы (Conversion operators: import)
Преобразующие-импортирующие операторы преобразуют не обобщенные последовательности в обобщенные (generic), пригодные для выполнения запросов последовательности:
- OfType— преобразует- IEnumerableв- IEnumerable<T>, отбрасывая элементы неподходящего типа
- Cast— преобразует- IEnumerableв- IEnumerable<T>, выбрасывая исключение если встречаются элементы неподходящего типа
Преобразующие-экспортирующие операторы (Conversion operators: export)
Преобразующие-экспортирующие операторы преобразуют последовательности в массивы, списки (list), словари (dictionary), форсируя немедленное выполнение:
- ToArray— преобразует- IEnumerable<T>в- T[]
- ToList— преобразует- IEnumerable<T>в- List<T>
- ToDictionary— преобразует- IEnumerable<T>в- Dictionary<TKey,TValue>
- ToLookup— преобразует- IEnumerable<T>в- ILookup<TKey,TElement>
- AsEnumerable— приводит последовательность к типу- IEnumerable<T>
- AsQueryable— приводит последовательность к типу- IQueryable<T>
Генерирующие операторы (Generation operators)
Генерирующие операторы создают последовательности:
- Empty— создает пустую последовательность
- Repeat— создает последовательность из повторяющихся элементов
- Range— создает целочисленную послеовательность
Цепочки операторов запроса
Можно объединять операторы запроса в цепочки и таким образом создавать более комплексные запросы:
| 1 2 3 4 5 6 7 8 | string[] names = { "Tom","Dick","Harry","Mary","Jay" }; IEnumerable<string> query = names     .Where (n => n.Contains ("a"))     .OrderBy (n => n.Length)     .Select (n => n.ToUpper()); foreach (string name in query) Console.Write (name + "|"); // Результат: JAY|MARY|HARRY| | 
Выполняются операторы в цепочках в порядке перечисления слева на право.
Запросы-выражения (Query Expressions)
Помимо краткого синтаксиса, рассмотренного выше и использующего для построения LINQ запросов методы расширения класса Enumerable, в C# также предусмотрен специальный язык для построения запросов, называемый запросы-выражения. Например, запрос из предыдущего примера можно записать так:
| 1 2 3 4 5 | IEnumerable<string> query =     from n in names     where n.Contains ("a")     orderby n.Length     select n.ToUpper(); | 
 Запрос-выражение всегда начинается с оператора from, а заканчивается либо оператором select, либо group. Все остальные, допустимые для запросов-выражений операторы, должны располагаться между ними в любой последовательности. Операторы в запросах LINQ располагаются несколько в иной последовательности нежели в SQL. LINQ операторы располагаются в порядке их выполнения.
Оператор from объявляет переменную диапазона (range variable, в примере это n), которая представляет собой отдельный элемент входящей последовательности и используется для того чтобы обойти ее, подобно переменной цикла foreach. Синтаксис оператора from в общем выглядит следующим образом:
| 1 | from тип переменная-диапазона in выражение-возвращающее-enumerable | 
Компилятор обрабатывает запросы-выражения переводя их в краткий синтаксис, а затем последовательно обрабатывает операторы запроса:
| 1 2 3 4 | IEnumerable<string> query = names     .Where (n => n.Contains ("a"))     .OrderBy (n => n.Length)     .Select (n => n.ToUpper()); | 
И запросы-выражения и краткие запросы имеют свои преимущества. Запросы-выражения поддерживают только ограниченный набор операторов:
- Where
- Select
- SelectMany
- OrderBy
- ThenBy
- OrderByDescending
- ThenByDescending
- GroupBy
- Join
- GroupJoin
При необходимости использовать другие операторы придется либо писать запрос используя только краткий синтаксис, либо создавать запросы со смешанным синтаксисом:
| 1 2 3 4 5 | string[] names = { "Tom","Dick","Harry","Mary","Jay" }; IEnumerable<string> query =     from n in names     where n.Length == names.Min (n2 => n2.Length)     select n; | 
 Запрос в примере вернет имена имеющие минимальную длину. Подзапрос (выделен жирным) как раз рассчитает эту минимальную длину. Поскольку оператор Min не поддерживается запросами-выражениями, для построения подзапроса используется краткий синтаксис. При этом для внешнего запроса вполне допустимо использовать синтаксис запросов-выражений.
Главным преимуществом запросов-выражений является то, что они кардинально упрощают сложные запросы благодаря следующим возможностям:
- использование оператора letпозволяет вводить новые переменные параллельно переменной диапазона
- использование нескольких генераторов (нескольких операторов from, эквивалентно оператору запросаSelectMany) непосредственно после объявления внешней переменной диапазона
- использование эквивалентов JoinиGroupJoinнепосредственно после объявления внешней переменной диапазона
Ключевое слово let
Ключевое слово let вводит новую переменную параллельно переменной диапазона:
| 1 2 3 4 5 6 7 | string[] names = { "Tom","Dick","Harry","Mary","Jay" }; IEnumerable<string> query =     from n in names     let vowelless = Regex.Replace (n, "[aeiou]", "")     where vowelless.Length > 2     orderby vowelless     select n + " - " + vowelless; | 
Запрос из примера вернет все имена, чья длина без гласных больше двух символов: Dick — Dck, Harry — Hrry, Mary — Mry.
Оператор let выполняет вычисления для каждого элемента, не изменяя при этом сам элемент. Запрос может содержать несколько операторов let, каждый из которых может быть дополнен операторами where и join.
Встречая оператор let компилятор создает временный анонимный тип, содержащий как исходный элемент (представленный переменной диапазона) так и измененный элемент (представленный переменной, введенной оператором let):
| 1 2 3 4 5 6 7 8 9 10 | IEnumerable<string> query = names     .Select (n => new         {             n = n,             vowelless = Regex.Replace (n, "[aeiou]", "")         }     )     .Where (temp0 => (temp0.vowelless.Length > 2))     .OrderBy (temp0 => temp0.vowelless)     .Select (temp0 => ((temp0.n + " - ") + temp0.vowelless)) | 
Запросы с продолжениями
В случае необходимости добавить после операторов select или group какие-либо другие операторы нужно использовать ключевое слово into, которое создает новую переменную диапазона и продолжает запрос:
| 1 2 3 4 5 | from c in "The quick brown tiger".Split() select c.ToUpper() into upper where upper.StartsWith ("T") select upper // Результат: "THE", "TIGER" | 
 Для операторов, идущих после ключевого слова into, предыдущая переменная диапазона уже не доступна.
Компилятор преобразует запросы с ключевым словом into просто в более длинные цепочки операторов:
| 1 2 3 4 | "The quick brown tiger".Split()     .Select (c => c.ToUpper())     .Where (upper => upper.StartsWith ("T")) // Завершающий Select(upper=>upper) будет опущен, т.к. он излишен | 
Множественные генераторы
Запрос может включать несколько генераторов — операторов from:
| 1 2 3 4 5 6 | int[] numbers = { 1, 2, 3 }; string[] letters = { "a", "b" }; IEnumerable<string> query =      from n in numbers     from l in letters     select n.ToString() + l; | 
 Результат будет такой же как при использовании вложенного цикла foreach:
| 1 | "1a", "1b", "2a", "2b", "3a", "3b" | 
 Если запрос содержит несколько операторов from, компилятор вызывает SelectMany:
| 1 2 3 | IEnumerable<string> query = numbers.SelectMany (     n => letters,     (n, l) => (n.ToString() + l)); | 
 SelectMany выполняет вложенные циклы. Метод является перегруженным и может принимать одно лямбда выражение или два. Он перечисляет все элементы входящей последовательности (в примере — numbers), выполняя для каждого из них первое (или единственное) лямбда выражение. Это лямбда выражение должно возвращать последовательность, которая сопоставляется с элементом входящей последовательности, формируя таким образом последовательность подпоследовательностей (в примере для каждого элемента входящей последовательности number первое лямбда выражение просто возвращает последовательность (массив) letters). Затем каждая подпоследовательность перечисляется и склеивается в одноуровневую последовательность. Если методу передано два лямбда выражения, то для каждого элемента исходящей последовательности выполняется второе лямбда выражение (в примере — n.ToString()+l).
Если впоследствии применить оператор where, можно отфильтровать и спроецировать результат как при использовании оператора join:
| 1 2 3 4 5 6 7 8 | string[] players = { "Tom", "Jay", "Mary" }; IEnumerable<string> query =     from name1 in players     from name2 in players     where name1.CompareTo (name2) < 0     orderby name1, name2     select name1 + " vs " + name2; // Результат: { "Jay vs Mary", "Jay vs Tom", "Mary vs Tom" } | 
Перевод такого запроса в короткий синтаксис более сложен и требует создания временных анонимных проекций. То, что это перевод будет выполнен компилятором автоматически, является одним из достоинств запросов-выражений.
В выражении второго генератора можно использовать первую переменную диапазона:
| 1 2 3 4 5 6 7 8 9 10 | string[] fullNames =     { "Anne Williams", "John Fred Smith", "Sue Green" }; IEnumerable<string> query =     from fullName in fullNames     from name in fullName.Split()     select name + " came from " + fullName; // Результат: // Anne came from Anne Williams // Williams came from Anne Williams // John came from John Fred Smith | 
Объединение (Joining)
В LINQ предусмотрено три оператора объединения, основными из них являются Joinи GroupJoin. Они поддерживают только часть функциональности предоставляемой множественными генераторами и оператором SelectMany, но являются более производительными поскольку используют основанную на хэш-таблицах технологию поиска в противовес вложенным циклам.
Join и GroupJoin поддерживают только эквивалентное объединение (т.е. объединяющее условие должно использовать оператор эквивалентности). Joinвозвращает в качестве результата одноуровневый набор, GroupJoin — многоуровневый (иерархический).
Синтаксис запроса-выражения join следующий:
| 1 2 3 | from внешняя-переменная in внешняя-последовательность join внутренняя-переменная in внутренняя-последовательность on внешний-ключ equals внутренний-ключ | 
Пример:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Внешняя последовательность: var customers = new[] {     new { ID = 1, Name = "Вася" },     new { ID = 2, Name = "Коля" },     new { ID = 3, Name = "Саша" } }; // Внутренняя последовательность: var purchases = new[] {     new { CustomerID = 1, Product = "дом" },     new { CustomerID = 2, Product = "корабль" },     new { CustomerID = 2, Product = "автомобиль" },     new { CustomerID = 3, Product = "мороженное" } }; // Запрос-выражение: IEnumerable<string> query =     from c in customers     join p in purchases on c.ID equals p.CustomerID     select c.Name + " bought a " + p.Product; // Компилятор переведет в краткий синтаксис: customers.Join ( // внешняя последовательность purchases, // внутренняя последовательность c => c.ID, // внешний ключ p => p.CustomerID, // внутренний ключ (c, p) => c.Name + " купил " + p.Product // результат ); /* Результат: Вася купил дом Коля купил корабль Коля купил автомобиль Саша купил мороженное */ | 
 Для локальных последовательностей Join и GroupJoin более эффективны при обработке больших коллекций, чем SelectMany, т.к. они сначала подгружают внутреннюю последовательность в индексированную хэш-таблицу. Для запросов к базе данных, однако, производительность всех запросов одинаковая, поэтому можно использовать, например, такой запрос:
| 1 2 3 4 | from c in customers from p in purchases where c.ID == p.CustomerID select c.Name + " bought a " + p.Product; | 
GroupJoin
GroupJoin делает то же самое, что и Join, но вместо одноуровневого результата он возвращает многоуровневый (иерархический) результат, сгруппированный по каждому внешнему элементу.
Синтаксис запроса-выражения для GroupJoin такой же как для Join, но с добавлением после оператора join ключевого слова into:
| 1 2 3 4 5 | IEnumerable<IEnumerable<Purchase>> query =     from c in customers     join p in purchases on c.ID equals p.CustomerID     into custPurchases     select custPurchases; // custPurchases - исходящая последовательность | 
 Оператор into переводится при компиляции в GroupJoin только если он идет непосредственно после оператора join. Если он идет после оператора select или
 group, он означает запрос с продолжением. Таким образом ключевое слово into в этих двух случаях используется абсолютно по-разному, но в обоих случаях оно вводит новую переменную запроса.
Результатом будет последовательность последовательностей, которую можно будет перечислить следующим образом:
| 1 2 3 | foreach (IEnumerable<Purchase> purchaseSequence in query)     foreach (Purchase p in purchaseSequence)         Console.WriteLine (p.Description); | 
Однако это не очень удобно, потому что внешняя последовательность не содержит ссылки на внешнего покупателя (customer). Поэтому удобнее было бы сделать так:
| 1 2 3 4 | from c in customers join p in purchases on c.ID equals p.CustomerID into custPurchases select new { CustName = c.Name, custPurchases }; | 
Тот же результат (правда менее эффективный для локальных запросов) можно получить создав проекцию в анонимный тип включающий подзапрос:
| 1 2 3 4 5 6 7 | from c in customers select new {     CustName = c.Name,     custPurchases =     purchases.Where (p => c.ID == p.CustomerID) } | 
Zip
Zip — простейший оператор объединения. Он перебирает две последовательности за один проход и возвращает последовательность, содержащую результаты выполнения функции (переданной в качестве аргумента) над парами элементов из двух последовательностей:
| 1 2 3 4 5 6 7 8 | int[] numbers = { 3, 5, 7 }; string[] words = { "three", "five", "seven", "ignored" }; IEnumerable<string> zip =     numbers.Zip (words, (n, w) => n + "=" + w); /* Результат: 3=three 5=five 7=seven */ | 
Лишние элементы в любой из входящих последовательностей игнорируются. Zip не применим для запросов к базе данных.
Сортировка (Ordering)
Ключевое слово orderby сортирует последовательность. Можно указать несколько выражений для сортировки:
| 1 2 3 4 5 6 7 | string[] names = { "Tom","Dick","Harry","Mary","Jay" }; IEnumerable<string> query =      from n in names     orderby n.Length, n     select n; // отсортирует имена по длине и затем по алфавиту: // Jay, Tom, Dick, Mary, Harry | 
 Встретив оператор orderby компилятор вызовет для первого его выражения метод OrderBy, а для всех последующих — метод ThenBy:
| 1 2 3 | IEnumerable<string> query = names     .OrderBy (n => n.Length)     .ThenBy (n => n) | 
 Оператор ThenBy уточняет, но не заменяет предыдущую сортировку.
По умолчанию сортировка выполняется по возрастанию. Чтобы отсортировать последовательность по убыванию нужно после любого из выражений оператора orderby добавить ключевое слово descending:
| 1 | orderby n.Length descending, n | 
 В этом случае компилятор вместо OrderByили ThenBy вызовет OrderByDescending или ThenByDescendingсоответственно:
| 1 | .OrderByDescending (n => n.Length).ThenBy (n => n) | 
 Сортирующие операторы возвращаю расширенный тип IEnumerable<T>— IOrderedEnumerable<T>. Он содержит функционал необходимый для оператора ThenBy.
Группировка (Grouping)
Оператор GroupBy группирует одноуровневую входящую последовательность в последовательность групп. Например, можно сгруппировать имена по их длине:
| 1 2 3 4 | string[] names = { "Tom","Dick","Harry","Mary","Jay" }; var query =      from name in names     group name by name.Length; | 
 Компилятор преобразует этот запрос в вызов метода GroupBy:
| 1 2 | IEnumerable<IGrouping<int,string>> query =     names.GroupBy (name => name.Length); | 
Результат можно перечислить следующим образом:
| 1 2 3 4 5 6 7 8 9 10 | foreach (IGrouping<int,string> grouping in query) {     Console.Write ("\r\n Length=" + grouping.Key + ":");     foreach (string name in grouping)         Console.Write (" " + name); } /* Результат: Length=3: Tom Jay Length=4: Dick Mary Length=5: Harry */ | 
 Оператор GroupBy создаст из элементов входящей последовательности временный словарь (dictionary) списков так, что все элементы с одинаковыми ключами будут собраны в одни подсписок. Затем он вернет последовательность групп. Группа (grouping) — это последовательность, у которой есть свойство Key:
| 1 2 3 4 5 6 | public interface IGrouping <TKey,TElement>     : IEnumerable<TElement>, IEnumerable {     // Свойство Key относится к подпоследовательности в целом:     TKey Key { get; } } | 
 По умолчанию элементы каждой группы — это никак не измененные элементы входящий последовательности. Однако их можно как-либо модернизировать, передав оператору group трансформирующее выражение — elementSelector:
| 1 2 | from name in names group name.ToUpper() by name.Length | 
 Методу GroupBy трансформирующее выражение передается в качестве второго аргумента:
| 1 2 3 | names.GroupBy (     name => name.Length,     name => name.ToUpper() ) | 
 Подпоследовательности не упорядочиваются по ключу, т.к. оператор GroupBy не выполняет сортировки (фактически сохраняется исходная сортировка). Поэтому для сортировки необходимо дополнительно вызвать метод OrderBy. В синтаксисе запроса-выражения в этом случае необходимо после оператора group by добавить оператор into, чтобы продолжить запрос, т.к. обычно group by завершает запрос:
| 1 2 3 4 | from name in names group name.ToUpper() by name.Length into grouping orderby grouping.Key select grouping | 
Запросы с продолжениями очень часто используются в группирующих запросах. Например, следующий запрос отбирает только группы с двумя элементами:
| 1 2 3 4 | from name in names group name.ToUpper() by name.Length into grouping where grouping.Count() == 2 select grouping | 
 Оператор where идущий после group by применяется к каждой подпоследовательности или группе в целом, а не к отдельным элементам.
OfType и Cast
Методы OfType и Cast преобразуют не обобщенные коллекции, реализующие интерфейс IEnumerable, в обобщенные коллекции интерфейса IEnumerable<T>, к которым в последствии можно выполнять запросы:
| 1 2 3 | var classicList = new System.Collections.ArrayList(); classicList.AddRange ( new int[] { 3, 4, 5 } ); IEnumerable<int> sequence1 = classicList.Cast<int>(); | 
 Различия в поведении этих методов проявляется при столкновении с элементами входящей последовательности, имеющих несовместимый тип: метод Cast в этом случае выбросит исключение, а метод OfType просто проигнорирует не совместимый элемент.
Логику оператора Cast можно реализовать и в запросах выражениях. Для этого просто нужно указать тип элемента сразу после ключевого слова from:
| 1 | from int x in classicList | 
| 1 | from x in classicList.Cast <int>() | 

