Содержание
- Основы LINQ
- Отложенное выполнение (Deferred Execution)
- Список стандартных операторов запроса
- Фильтрующие операторы (Filtering operators)
- Проецирующие операторы (Projection operators)
- Объединяющие операторы (Joining operators)
- Упорядочивающие операторы (Ordering operators)
- Группирующие операторы (Grouping operators)
- Операторы комплектовщики (Set operators)
- Элементные операторы (Element operators)
- Агрегирующие операторы (Aggregation operators)
- Операторы кванторы (Quantifiers)
- Преобразующие-импортирующие операторы (Conversion operators: import)
- Преобразующие-экспортирующие операторы (Conversion operators: export)
- Генерирующие операторы (Generation operators)
- Цепочки операторов запроса
- Запросы-выражения (Query Expressions)
- Ключевое слово let
- Запросы с продолжениями
- Множественные генераторы
- Объединение (Joining)
- Сортировка (Ordering)
- Группировка (Grouping)
- OfType и Cast
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>() |