Speak.Me Учить иностранные слова

LINQ C#

Содержание

LINQ — Language Integrated Query (Внутриязыковой запрос) — технология, представляющая собой набор функций, позволяющих писать структурированные типобезопасные запросы к локальным объектам-коллекциям и удаленным источникам данных.

С помощью LINQ можно писать запросы к любой коллекции, реализующей интерфейсIEnumerable<>, например, к массивам, спискам (list), XML DOM, удаленным источникам данных, таким как таблицы SQL сервера. LINQ объединяет преимущества динамических запросов и проверки типов при компиляции.

Основы LINQ

Базовыми единицами данных в LINQ являются последовательности и элементы. Последовательность — любой объект, реализующий обобщенный интерфейс IEnumerable, элемент — каждая единица последовательности. В следующем примере names — последовательность, а Tom, Dick, и Harry — элементы:

Последовательности как эта называются локальными, т.к. представляют собой локальные коллекции объектов, расположенные в памяти.

Оператор запроса (query operator) — метод, преобразующий последовательность. Обычно операторы запроса принимают входящую последовательность и возвращают преобразованную исходящую последовательность. Класс Enumerable в пространстве имен System.Linq содержит около 40 операторов запроса; все они являются статическими методами расширениями (extension methods). Эти 40 операторов называютсястандартными операторами запроса.

LINQ также поддерживает последовательности, которые могут быть динамически извлечены из удаленных источников данных, таких как SQL сервер. Эти последовательности дополнительно реализуют интерфейс IQueryable<>, а указанные стандартные операторы запроса для них определяются в классе Queryable.

Простой запрос, оператор Where

Запрос представляет собой выражение, которое преобразует последовательности с помощью одного или нескольких операторов запроса. Простейший запрос включает одну входящую последовательность и один оператор запроса. Например, с помощью оператора Where можно извлечь из простого массива все имена, чья длина как минимум 4 символа:

Т.к. стандартные операторы запроса реализованы как методы расширения, мы можем вызвать Where непосредственно у массива names так, как если бы он был экземплярным методом:

В последнем примере необходимо импортировать пространство имен System.Linq с помощью директивы using иначе код не скомпилируется.

Метод Where класса System.Linq.Enumerable имеет следующую сигнатуру:

Здесь source — входящая последовательность, а predicate — делегат, который вызывается для каждого его элемента. Метод Where в исходящую последовательность включит все элементы, для которых делегат вернет true. Внутренне он реализуется с помощью итератора:

Проецирование (Projecting), оператор Select

Другой основополагающий оператор запроса — метод Select. Он преобразует (проецирует) каждый элемент входящей последовательности с помощью лямбда выражения:

Запрос может проецировать анонимный тип:

Операторы Take и Skip

Исходная очередность элементов в входящей последовательности в LINQ имеет существенное значение. Некоторые операторы запроса, такие как Take,Skip и Reverse, способны оказывать влияние на эту очередность.

Оператор Take возвращает в исходящей последовательности первые n элементов, отбрасывая остальные:

Оператор Skip, напротив, отбрасывает первые n элементов и возвращает в исходящей последовательности оставшиеся:

Элементные операторы (Element operators)

Не все операторы запроса возвращают последовательность. Элементные операторы, например: First, Last, Single и ElementAt, извлекают один элемент из входящей последовательности.

Все эти операторы выбрасывают исключение, если последовательность не содержит подходящего элемента. Чтобы вместо исключения получить null (пустой результат), нужно использовать соответственно операторы FirstOrDefault, LastOrDefault,SingleOrDefault или ElementAtOrDefault.

Операторы Single и SingleOrDefault эквивалентны операторам Firstи FirstOrDefault, за исключением того, что они выбрасывают исключение если подходящих элементов больше одного.

Агрегирующие операторы (Aggregation operators)

Агрегирующие операторы возвращают скалярное значение как правило числового типа. Наиболее часто используются агрегирующие операторы Count, Min, Max и Average.

Оператор Count подсчитывает количество элементов в последовательности. Ему можно передать факультативный предикат, тогда оператор посчитает только те элементы, для которых предикат вернет true:

Операторы Min, Max и Average также могут принимать факультативный аргумент, который преобразует каждый элемент последовательности перед тем, как выполнить агрегацию:

Кванторы (Quantifiers)

Квантификацией в логике (от лат. quantum — сколько) называется задание пределов, определяющих количество объектов, для которых применимо выражение. Широко применяются два квантора: существования (имеется такой) и всеобщности (для всех, для любого).

Операторы кванторы в LINQ возвращают логическое (bool) значение. К ним относятся Contains, Any, All, and SequenceEquals (последний сравнивает две последовательности):

Операторы комплектовщики (Set operators)

Операторы комплектовщики принимают две входящие последовательности одного типа. Оператор Concat присоединяет одну последовательность к другой. Оператор Unionделает тоже самое, но при этом удаляет повторы.

Также к этой категории относятся операторы Intersect и Except:

Отложенное выполнение (Deferred Execution)

Важной особенностью многих операторов запроса является то, что они выполняются не в момент их создания, а в момент перечисления исходящей последовательности (т.е. когда вызывается метод MoveNext их нумератора):

В этом примере дополнительное число, которое мы вставили в список после создания запроса также было включено в результат, поскольку результат был сформирован только во время выполнения инструкции foreach. Эта особенность получила названиеотложенное (deferred) или ленивое (lazy) выполнение. Отложенное выполнение отделяет создание запросов от их выполнения, позволяя создавать запросы в несколько шагов. Все стандартные операторы запроса предполагают отложенное выполнение, за следующим исключением:

  • операторы, возвращающие отдельные элементы или скалярные значения (элементные операторы (element operators), операторы объединения (aggregation operators) и операторы кванторы (quantifiers))
  • следующие операторы преобразования (conversion operators): ToArray, ToList,ToDictionary, ToLookup

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

Вложенные запросы (subqueries) позволяют обойти указанные выше исключения из отложенного выполнения. Все вложенные запросы выполняются в момент выполнения основного запроса. Поэтому если основной запрос выполняется отложено, то и все его вложенные запросы, в том числе объединяющие и преобразующие операторы, выполняются отложено:

Список стандартных операторов запроса

Стандартные операторы запроса, реализованные в классе 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 — возвращают элементы отсортированные в возрастающем порядке
  • OrderByDescendingThenByDescending — возвращают элементы отсортированный в убывающем порядке
  • Reverse — возвращает элементы в обратном порядке

Группирующие операторы (Grouping operators)

Группирующие операторы группируют последовательность в подпоследовательности:

  • GroupBy — группирует элементы последовательности в подмножества (подпоследовательности)

Операторы комплектовщики (Set operators)

Операторы комплектовщики принимают две последовательности одного типа и возвращают их общность, совокупность или разницу:

  • Concat — объединяет две последовательности
  • Union — объединяет две последовательности, удаляя повторы
  • Intersect — возвращает элементы, присутствующие в обеих последовательностях
  • Except — возвращает элементы первой последовательности, отсутствующие во второй

Элементные операторы (Element operators)

Элементные операторы выбирают отдельный элемент из последовательности:

  • First, FirstOrDefault — возвращают первый элемент последовательности или первый элемент, удовлетворяющий переданному предикату
  • Last, LastOrDefault — возвращают последний элемент последовательности или последний элемент, удовлетворяющий переданному предикату
  • SingleSingleOrDefault — эквивалент First/FirstOrDefault, но если совпадений больше одного, выбрасывает исключение
  • ElementAtElementAtOrDefault — возвращает элемент с указанной позицией
  • 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 — создает целочисленную послеовательность

Цепочки операторов запроса

Можно объединять операторы запроса в цепочки и таким образом создавать более комплексные запросы:

Выполняются операторы в цепочках в порядке перечисления слева на право.

Запросы-выражения (Query Expressions)

Помимо краткого синтаксиса, рассмотренного выше и использующего для построения LINQ запросов методы расширения класса Enumerable, в C# также предусмотрен специальный язык для построения запросов, называемый запросы-выражения. Например, запрос из предыдущего примера можно записать так:

Запрос-выражение всегда начинается с оператора from, а заканчивается либо оператором select, либо group. Все остальные, допустимые для запросов-выражений операторы, должны располагаться между ними в любой последовательности. Операторы в запросах LINQ располагаются несколько в иной последовательности нежели в SQL. LINQ операторы располагаются в порядке их выполнения.

Оператор from объявляет переменную диапазона (range variable, в примере это n), которая представляет собой отдельный элемент входящей последовательности и используется для того чтобы обойти ее, подобно переменной цикла foreach. Синтаксис оператора from в общем выглядит следующим образом:

Компилятор обрабатывает запросы-выражения переводя их в краткий синтаксис, а затем последовательно обрабатывает операторы запроса:

И запросы-выражения и краткие запросы имеют свои преимущества. Запросы-выражения поддерживают только ограниченный набор операторов:

  • Where
  • Select
  • SelectMany
  • OrderBy
  • ThenBy
  • OrderByDescending
  • ThenByDescending
  • GroupBy
  • Join
  • GroupJoin

При необходимости использовать другие операторы придется либо писать запрос используя только краткий синтаксис, либо создавать запросы со смешанным синтаксисом:

Запрос в примере вернет имена имеющие минимальную длину. Подзапрос (выделен жирным) как раз рассчитает эту минимальную длину. Поскольку оператор Min не поддерживается запросами-выражениями, для построения подзапроса используется краткий синтаксис. При этом для внешнего запроса вполне допустимо использовать синтаксис запросов-выражений.

Главным преимуществом запросов-выражений является то, что они кардинально упрощают сложные запросы благодаря следующим возможностям:

  • использование оператора let позволяет вводить новые переменные параллельно переменной диапазона
  • использование нескольких генераторов (нескольких операторов from, эквивалентно оператору запроса SelectMany) непосредственно после объявления внешней переменной диапазона
  • использование эквивалентов Join и GroupJoin непосредственно после объявления внешней переменной диапазона

Ключевое слово let

Ключевое слово let вводит новую переменную параллельно переменной диапазона:

Запрос из примера вернет все имена, чья длина без гласных больше двух символов: Dick — Dck, Harry — Hrry, Mary — Mry.

Оператор let выполняет вычисления для каждого элемента, не изменяя при этом сам элемент. Запрос может содержать несколько операторов let, каждый из которых может быть дополнен операторами where и join.

Встречая оператор let компилятор создает временный анонимный тип, содержащий как исходный элемент (представленный переменной диапазона) так и измененный элемент (представленный переменной, введенной оператором let):

Запросы с продолжениями

В случае необходимости добавить после операторов select или group какие-либо другие операторы нужно использовать ключевое слово into, которое создает новую переменную диапазона и продолжает запрос:

Для операторов, идущих после ключевого слова into, предыдущая переменная диапазона уже не доступна.

Компилятор преобразует запросы с ключевым словом into просто в более длинные цепочки операторов:

Множественные генераторы

Запрос может включать несколько генераторов — операторов from:

Результат будет такой же как при использовании вложенного цикла foreach:

Если запрос содержит несколько операторов from, компилятор вызывает SelectMany:

SelectMany выполняет вложенные циклы. Метод является перегруженным и может принимать одно лямбда выражение или два. Он перечисляет все элементы входящей последовательности (в примере — numbers), выполняя для каждого из них первое (или единственное) лямбда выражение. Это лямбда выражение должно возвращать последовательность, которая сопоставляется с элементом входящей последовательности, формируя таким образом последовательность подпоследовательностей (в примере для каждого элемента входящей последовательности number первое лямбда выражение просто возвращает последовательность (массив) letters). Затем каждая подпоследовательность перечисляется и склеивается в одноуровневую последовательность. Если методу передано два лямбда выражения, то для каждого элемента исходящей последовательности выполняется второе лямбда выражение (в примере — n.ToString()+l).

Если впоследствии применить оператор where, можно отфильтровать и спроецировать результат как при использовании оператора join:

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

В выражении второго генератора можно использовать первую переменную диапазона:

Объединение (Joining)

В LINQ предусмотрено три оператора объединения, основными из них являются Joinи GroupJoin. Они поддерживают только часть функциональности предоставляемой множественными генераторами и оператором SelectMany, но являются более производительными поскольку используют основанную на хэш-таблицах технологию поиска в противовес вложенным циклам.

Join и GroupJoin поддерживают только эквивалентное объединение (т.е. объединяющее условие должно использовать оператор эквивалентности). Joinвозвращает в качестве результата одноуровневый набор, GroupJoin — многоуровневый (иерархический).

Синтаксис запроса-выражения join следующий:

Пример:

Для локальных последовательностей Join и GroupJoin более эффективны при обработке больших коллекций, чем SelectMany, т.к. они сначала подгружают внутреннюю последовательность в индексированную хэш-таблицу. Для запросов к базе данных, однако, производительность всех запросов одинаковая, поэтому можно использовать, например, такой запрос:

GroupJoin

GroupJoin делает то же самое, что и Join, но вместо одноуровневого результата он возвращает многоуровневый (иерархический) результат, сгруппированный по каждому внешнему элементу.

Синтаксис запроса-выражения для GroupJoin такой же как для Join, но с добавлением после оператора join ключевого слова into:

Оператор into переводится при компиляции в GroupJoin только если он идет непосредственно после оператора join. Если он идет после оператора select или
group, он означает запрос с продолжением. Таким образом ключевое слово into в этих двух случаях используется абсолютно по-разному, но в обоих случаях оно вводит новую переменную запроса.

Результатом будет последовательность последовательностей, которую можно будет перечислить следующим образом:

Однако это не очень удобно, потому что внешняя последовательность не содержит ссылки на внешнего покупателя (customer). Поэтому удобнее было бы сделать так:

Тот же результат (правда менее эффективный для локальных запросов) можно получить создав проекцию в анонимный тип включающий подзапрос:

Zip

Zip — простейший оператор объединения. Он перебирает две последовательности за один проход и возвращает последовательность, содержащую результаты выполнения функции (переданной в качестве аргумента) над парами элементов из двух последовательностей:

Лишние элементы в любой из входящих последовательностей игнорируются. Zip не применим для запросов к базе данных.

Сортировка (Ordering)

Ключевое слово orderby сортирует последовательность. Можно указать несколько выражений для сортировки:

Встретив оператор orderby компилятор вызовет для первого его выражения метод OrderBy, а для всех последующих — метод ThenBy:

Оператор ThenBy уточняет, но не заменяет предыдущую сортировку.

По умолчанию сортировка выполняется по возрастанию. Чтобы отсортировать последовательность по убыванию нужно после любого из выражений оператора orderby добавить ключевое слово descending:

В этом случае компилятор вместо OrderByили ThenBy вызовет OrderByDescending или ThenByDescendingсоответственно:

Сортирующие операторы возвращаю расширенный тип IEnumerable<T>— IOrderedEnumerable<T>. Он содержит функционал необходимый для оператора ThenBy.

Группировка (Grouping)

Оператор GroupBy группирует одноуровневую входящую последовательность в последовательность групп. Например, можно сгруппировать имена по их длине:

Компилятор преобразует этот запрос в вызов метода GroupBy:

Результат можно перечислить следующим образом:

Оператор GroupBy создаст из элементов входящей последовательности временный словарь (dictionary) списков так, что все элементы с одинаковыми ключами будут собраны в одни подсписок. Затем он вернет последовательность групп. Группа (grouping) — это последовательность, у которой есть свойство Key:

По умолчанию элементы каждой группы — это никак не измененные элементы входящий последовательности. Однако их можно как-либо модернизировать, передав оператору group трансформирующее выражение — elementSelector:

Методу GroupBy трансформирующее выражение передается в качестве второго аргумента:

Подпоследовательности не упорядочиваются по ключу, т.к. оператор GroupBy не выполняет сортировки (фактически сохраняется исходная сортировка). Поэтому для сортировки необходимо дополнительно вызвать метод OrderBy. В синтаксисе запроса-выражения в этом случае необходимо после оператора group by добавить оператор into, чтобы продолжить запрос, т.к. обычно group by завершает запрос:

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

Оператор where идущий после group by применяется к каждой подпоследовательности или группе в целом, а не к отдельным элементам.

OfType и Cast

Методы OfType и Cast преобразуют не обобщенные коллекции, реализующие интерфейс IEnumerable, в обобщенные коллекции интерфейса IEnumerable<T>, к которым в последствии можно выполнять запросы:

Различия в поведении этих методов проявляется при столкновении с элементами входящей последовательности, имеющих несовместимый тип: метод Cast в этом случае выбросит исключение, а метод OfType просто проигнорирует не совместимый элемент.

Логику оператора Cast можно реализовать и в запросах выражениях. Для этого просто нужно указать тип элемента сразу после ключевого слова from:

Компилятор переделает это на: