Стандартные операторы запроса с LINQ

ОГЛАВЛЕНИЕ

Язык LINQ позволяет разработчикам включать в код Microsoft® .NET Framework 3.5 SQL-подобные запросы, используя строго типизированный синтаксис. Различные поставщиеи LINQ, в частности методы «LINQ — объекты» (позволяет писать запросы по иерархии объектов) и «LINQ — сущности» (позволяет писать запросы по концептуальной модели Entity Framework), в таком случае способны эффективно обрабатывать запросы, учитывая особенности хранилищ данных.

Кроме строго типизированного синтаксиса запросы LINQ имеют в своем арсенале большое количество стандартных операторов, расширяющих функциональность запросов. Стандартные операторы применяются к последовательности и позволяют, к примеру, определить, входит ли данное значение в последовательность, или выполнить над членами последовательности какую-либо операцию, скажем сложение.

В этом выпуске мы на практике рассмотрим выполнение запросов и операций при помощи LINQ, как в методе «LINQ — объекты», так и в методе «LINQ — сущности». Запросы будут выполняться по одной коллекции сущностей. При перемещении по иерархии будут использоваться навигационные свойства сущностей. Мы также коснемся порядка применения различных стандартных операторов к массивам и коллекциям. Мы продемонстрируем, как можно дополнить стандартные ооператоры LINQ лямбда-выражениями и как их использовать для анализа информации, имеющейся в последовательности, и для выполнения сложных логических операций над ее членами. Все примеры кода можно загрузить (см. msdn.microsoft.com/msdnmag/code08.aspx).


Операторы и LINQ

LINQ — сам по себе очень мощное средство, независимо от того, какой применяется методо: «LINQ — XML», «LINQ — DataSets», «LINQ — сущности», «LINQ — объекты» или какой-то другой. Главное преимущество LINQ — строго типизированный синтаксис запросов, который может быть использован в любом их перечисленных методов. Если LINQ применяется в сочетании с одним или несколькими стандартными операторами запроса, формируется еще более функциональный набор средств, позволяющих четко контролировать набор данных.

Стнадартные операторы запросов располагаются в сборке System.Core.dll в пространстве имен System.Linq и представляют собой методы расширений статических классов Enumerable и Queryable. Применять их можно к объектам, реализующим интерфейсы IEnumerable<T> и IQueryable<T>. Это означает, что операторы применяются к самым различным классам, начиная с коллекций и массивов (последовательностей), размещенных в оперативной памяти, и заканчивая удаленными базами данных, использующими поставщики «LINQ — сущности» или «LINQ — SQL».

Определить, какой набор операторов можно использовать для выполнения той или иной задачи, несложно. Если оператор будет включен в запрос LINQ, то применять можно любые операторы из списка методов расширения статического класса Queryable. Если оператор будет применяться к последовательности, реализующей интерфейс IEnumerable<T>, использовать можно операторы из списка методов расширения класса Enumerable. Однако имейте в виду, что некоторые операторы, относящиеся к классу Queryable, можно применять только к определенным хранилищам данных, и поэтому во время выполнения они могут не поддерживаться.


Типы операторов

Существует много различных типов операторов (любой из них можно найти в классаых Enumerable и Queryable, воспользовавшись обозревателем объектов). На рис. A приведены типы операторов в алфавитном порядке. Он дает представление о том, какие функции операторы могут выполнять. Преимущества некоторых из них мы продемонстрируем на примере их применения в методах «LINQ — объекты» и «LINQ — сущности».

Figure A Категории операторов

ОператорОписание
Объединение
AggregateПрименяет к последовательности пользовательский метод.
AverageВычисляет среднее для числовой последовательности.
CountВозвращает количество элементов в последовательности (целочисленное значение).
LongCountВозвращает количество элементов в последовательности (значение в диапазоне LongInt).
MinВозвращает наименьшее значение для числовой последовательности.
MaxВозвращает наибольшее значение для числовой последовательности.
SumСкладывает члены числовой последовательности.
Конкатенация
ConcatСоединяет две последовательности в одну.
Преобразование
CastПреобразует элементы последовательности в элемены указанного типа.
OfTypeВыбирает из элементов последовательности элемены указанного типа.
ToArrayВозвращает массив из элементов последовательности.
ToDictionaryВозвращает словарь из элементов последовательности.
ToListВозвращает список из элементов последовательности.
ToLookupВозвращает результаты поиска по последовательности.
ToSequenceВозвращает последовательность IEnumerable.
Элемент
DefaultIfEmptyСоздает стандартный элемент для пустой последовательности.
ElementAtВозвращает элемент последовательности по указанному индексу.
ElementAtOrDefaultВозвращает элемент по указанному индексу или стандартный элемент (если индекс вышел за пределы диапазона).
FirstВозвращает первый элемент последовательности.
FirstOrDefaultВозвращает первый элемент последовательности или стандартный элемент (если нужный элемент не найден).
LastВозвращает последний элемент последовательности.
LastOrDefaultВозвращает последний элемент последовательности или стандартный элемент (если нужный элемент не найден).
SingleВозвращает единственный элемент последовательности.
SingleOrDefaultВозвращает единственный элемент последовательности или стандартный элемент (если нужный элемент не найден).
Равенство
SequenceEqualПроверяет эквивалентность двух последовательностей.
Создание
EmptyСоздает пустую последовательность.
RangeСоздает последовательность в соответствии с заданным диапазоном.
RepeatСоздает последовательность, повторяя значение заданное количество раз.
Группировка
GroupByГруппирует элементы последовательности указанным образом.
Присоединение
GroupJoinВыполняет группированное соединение двух последовательностей.
JoinВыполняет внутреннее соединение двух последовательностей.
Упорядочение
OrderByУпорядочивает элементы последовательности по заданным значениям в порядке возрастания.
OrderByDescendingУпорядочивает элементы последовательности по заданным значениям в порядке убывания.
ThenByУпорядочивает элементы уже упорядоченной последовательности в порядке возрастания.
ThenByDescendingУпорядочивает элементы уже упорядоченной последовательности в порядке убывания.
ReverseЗеркально отображает порядок расположения элементов в последовательности.
Разделение на части
SkipВозвращает последовательность, в которой указанное число элементов пропущено.
SkipWhileВозвращает последовательность, в которой пропущены элементы, не соответствующие указанному условию.
TakeВозвращает последовательность, в которую включается указанное число элементов.
TakeWhileВозвращает последовательность, в которую включаются элементы, соответствующие указанному условию.
Проекция
SelectСоздает проекцию части последовательности.
SelectManyСоздает проекцию части последовательности по принципу «один ко многим».
Кванторы
AllОпределяет соответствие всех элементов последовательности указанным условиям.
AnyОпределяет, есть ли в последовательность элементы, удовлетворяющие указанным условиям.
ContainsОпределяет, есть ли в последовательности указанный элемент.
Ограничение
WhereСортирует члены последовательности.
Настройка
DistinctВозвращает последовательность без повторяющихся элементов.
ExceptВозвращает последовательность, представляющую собой разность двух других последовательностей.
IntersectВозвращает последовательность, представляющую собой пересечение двух других последовательностей.
UnionВозвращает последовательность, представляющую собой объединение двух других последовательностей.


Лямбда-выражения

Многие стандартные операторы для обработки отдельных элементов последовательности используют делегаты Func. Лямбда-выражения могут использоваться совместно со стандартными операторами запросов для представления делегатов. Лямбда-выражение — это сокращенный вариант реализации делегата, и использовать их можно везде, где можно использовать анонимные делегаты. Лямбда-выражения поддерживаются и в C#, и в Visual Basic® .NET. Здесь нужно отметить, что поскольку Visual Basic .NET не позволяет применять анонимные методы, в этой платформе лямбда-выражения могут состояить только из одного выражения.

Рассмотрим порядок применения оператора Single к массиву целых чисел. Создадим массив, каждый элементо которого представляет собой степень двойки (от 1 до 10). Оператор Single будет использоваться для получения элемента, удовлетворяющего условиям, заданным в лямбда-выражении:

int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int singleNum = nums.Single(x => x > 16 && x < 64);
Console.WriteLine(singleNum.ToString());

Лямбда-выражение состоит из нескольких важных частей. Начинается оно с определения переменной, передаваемой в делегат. В нашем примере переменная x (объявленная слева от оператора =>) является аргуметом, представляющим передаваемый элемент массива. Остальная часть лямбда-выражения описывает логику оценки элементов. Такого же поведения можно добиться при помощи анонимного делегата:

int singleNum = nums.Single<int>(
  delegate(int x) {return (x > 16 && x < 64); }
);

Недостаток такого кода в том, что его труднее воспринимать, чем код с использованием лямбда-выражений. С появлением C# 2.0 анонимных делегатов передача делегатов упростилась, однако лямбда-выражения, благодаря компактному синтаксису, сделали ее просто элементарной.


First и Single

Если нужно выбрать в последовательности одно значение, удобно применять операторы First, FirstOrDefault, Single и SingleOrDefault. Метод First возвращает первый элемент последовательность. В него можно передать лямбда-выражение, задающее определенные условия. Например, если нужно в последовательности целых чисел найти первое значение, превышающее 50, можно использовать следующий код:

int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int num1 = nums.First<int>();
int num2 = nums.First<int>(x => x > 50);
int num3 = nums.FirstOrDefault<int>(x => x > 5000);

Console.WriteLine(
  num1.ToString() + "-" + 
  num2.ToString() + "-" + 
  num3.ToString());

Это фрагмент кода возвращает первый элемент (1), первый элемент, превышающий 50, (64) и первый элемент, превышающий 5000. Поскольку элемента, удовлетворяющего третьему условию, нет (в последовательности нет чисел больше 5000), то в случае использования оператора First (а не FirstOrDefault) выдается исключение. При использовании оператора FirstOrDefault, если нет элементов, удовлетворяющих условиям лямбда-выражения, код возвращает 0. Оператор First можно также использовать в запросах типа «LINQ — сущности»:

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
  select c).First(c => c.City.Equals("London"));
  Console.WriteLine(query.CompanyName);

В данном примере фрагмент кода возвращает имя первого клиента лондонского офиса компании. Как мы видим, в разных поставщиках LINQ («LINQ — объекты» и «LINQ — сущности» в нашем случае) используется один и тот же синтаксис.

Оператор First удобно применять в контексте «LINQ — сущности», особенно если заранее известно, что запрос вернет только одну запись. Например, можно создать запрос, который будет находить клиента по идентификатору. В таком случае запрос может вернуть либо одну запись, либо ни одной, и здесь последовательность немногим полезнее самой сущности. Другими словами, можно использовать саму сущность Customer, а не последовательность, состоящую из 1 сущности. Это один из тех случаев, в которых метод First приходится кстати (см. фрагмент кода, приведенный ниже). Поскольку Entity Framework даже не пытается распределить выполнение одного запроса между клиентом и сервером, метод Single в методе «LINQ — сущности» не поддерживается — используется метод First.

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
  where c.CustomerID.Equals("BOLID")
  select c).First();
  Console.WriteLine(query.CompanyName);


Статистические выражения, иерархии и проекции

Применение статистических операторов, таких как Sum, в методе «LINQ — сущности» может значительно упростить запрос. Например, следующий фрагмент кода позволяет получить последовательность заказов на общую сумму более 10 000 долларов США:

using (Entities entities = new Entities())
{
  var query = from o in entities.Orders
  where o.OrderDetails.Sum(
  od => od.UnitPrice * od.Quantity) >= 10000
  select o;
  foreach (Orders order in query)
  Console.WriteLine(order.OrderID);
}

Поскольку LINQ позволяет выполнять запросы по иерархическим коллекциям сущностей, стандартные операторы запросов тоже могут применяться ко вложенным последовательностям. Это может пригодиться в том случае, если нужно выполнять вычисления с производными данными или опрашивать их. Производные данные существуют только в форме набора базовых данных. В качестве примера можно привести заказы, в данных которых указана только цена и количество. Общая сумма заказа нигде не указывается. Применяя оператор Sum к запросу LINQ, можно, к примеру, получить список клиентов, потративших более 20 000 долларов на заказы:

using (Entities entities = new Entities())
{
  var query = from c in entities.Customers
  where c.Orders.Sum(
  o => o.OrderDetails.Sum(
  od => od.UnitPrice * od.Quantity)) >= 25000
  select c;
  foreach (Customers customer in query)
  Console.WriteLine(customer.CompanyName);

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

Оператор Count — еще один пример стандартного статистического оператора. Он позволяет, к примеру, узнать количество клиентов, потративших более 25 000 долларов:

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
  where c.Orders.Sum(
  o => o.OrderDetails.Sum(
  od => od.UnitPrice * od.Quantity)) >= 25000
  select c).Count();
  Console.WriteLine(query);

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

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
  select new
  {
  c.CustomerID, 
  Total = c.Orders.Sum(
  o => o.OrderDetails.Sum(od => od.UnitPrice))
  }).Max(c2 => c2.Total);
  Console.WriteLine(query);


Сортировки и проекции

Возможно, вы заметили, что в предыдущем примере мы использовали проекцию. Запрос LINQ возвращает список клиентов только после использования оператора Max. До этого возвращается проекция, которая создает новую сущность, имеющую свойство CustomerID и свойство Total (соответствующее той сумме, которую потратил клиент). Проекции — это компонент LINQ, и если они вопрощаются в последовательности, как в предыдущем примере, к ним можно после этого применять стандартные операторы запросов.

На рис. 1 показана процедура создания проекции новой сущности, содержащей идентификатор клиента и общую сумму его заказов (используется оператор Sum, как было описано выше). На рис. 1 также используется оператор OrderByDescending — для упорядочения проекций сущностей по вычисленной итоговой сумме. Если у двух клиентов оказывается одинаковая сумма, можно использовать дополнительный оператор упорядочения. Например, для введения дополнительного критерия сортировки, в оператор foreach, приведенный на рис. 1, можно добавить следующий код:

Figure 1 Aggregates, Projections, and Ordering

using (Entities entities = new Entities())
{
  var query = from c in entities.Customers
  where c.Orders.Sum(
  o => o.OrderDetails.Sum(od => od.UnitPrice)) > 0
  select new
  {
  c.CustomerID, 
  Total = c.Orders.Sum(
  o => o.OrderDetails.Sum(od => od.UnitPrice))
  };
  foreach (var item in query.OrderByDescending(x => x.Total))
  Console.WriteLine(item.CustomerID + " == " + item.Total);
}

foreach (var item in 
  query.OrderByDescending(x => x.Total)
  .ThenBy(x => x.CustomerID))
{
  Console.WriteLine(item.CustomerID + " == " + item.Total);
}

В данном фрагменте кода мы использовали оператор ThenBy и лямбда-выражение. В результате первым критерием сортировки последовательности является общая сумма заказов (по убыванию), а вторым — идентификатор CustomerID, имеющийся в проекции (по убыванию).


Преобразования и кванторы

Если нужно определить, присутствует ли то или иное значение в последовательности, можно использовать стандартный оператор Any. Кванторы (Any, All, Contains) позволяют просмотреть последовательность элементов и определить, соответствует она условиям лямбда-выражения или нет. Их удобно использовать, если, к примеру, нужно определить, существует ли клиент, проживающий по указанному адресу, в одной стране живут клиенты или нет, и так далее.

Запрос LINQ, приведенный ниже, позволяет проверить, все ли британские подданные из числа клиентов компании живут в Лондоне. Здесь используется квантор All и лямбда-выражение, определяющее, указан Лондон в качестве города проживания или нет. Если все элементы последовательности соответствуют этому критерию, то оператор All возвращает значение true:

using (Entities entities = new Entities())
{
  bool allUKCustomerAreFromLondon = (from c in entities.Customers
  where c.Country == "UK"
  select c).All(
  c => c.City.Equals("London"));
  Console.WriteLine(allUKCustomerAreFromLondon ? "Yes" : "No");

Еще один пример: есть ли среди британских клиентов компании люди, живущие в городе Каус. В этом случае используется оператор Any:

using (Entities entities = new Entities())
{
  bool isOneUKCustomerFromCowes = (from c in entities.Customers
  where c.Country == "UK"
  select c).Any(
  c => c.City.Equals("Cowes"));
  Console.WriteLine(isOneUKCustomerFromCowes? "Yes" : "No");
}

Оператор Contains по принципу действия схож с оператором Any: он тоже позволяет определить, есть ли в последовательности указанный элемент. Разница в том, что оператор Any определяет, присутствует ли определенное значение в элементе последовательности, а оператор Contains — присутствует ли в последовательности экземпляр элемента. Например, прежде чем добавить объект в последовательность, можно проверить, нет ли его там уже. На рис. 2, показано, как это сделать.

Figure 2 Using Contains and Conversion

using (Entities entities = new Entities())
{
  Customers customerBSBEV = (from c in entities.Customers
  where c.CustomerID == "BSBEV"
  select c).First();

  var customersUK = from c in entities.Customers
  where c.Country == "UK"
  select c;

  bool isCustomerInSequence = customersUK.Contains(customerBSBEV);

  Console.WriteLine(isCustomerInSequence? "Yes" : "No");
}

Обратите внимание, что в этом примере сущность Customers мы получаем для клиента BSBEV. Затем мы получаем последовательность сущностей Customer, соответствующих клиентам, проживающим в Великобритании. И наконец, оператор Contains проверяет, есть ли в последовательности экземпляр, присвоенный в качестве значения переменной customerBSBEV.

Вариант использования оператора Contains, показанный на рис. 2, применим тогда, когда наверняка известно, что объекты можно будет сравнить на основании экземпляров. А что если проверку нужно провести на основании логической идентификации? Оператор Contains позволяет передать объект, реализуя интерфейс IEqualityComparer<T>. Если оператор Contains должен работать на основании идентификатора клиента, код, приведенный на рис. 2, можно переписать следующим образом:

using (Entities entities = new Entities())
{
  ...

  bool isCustomerInSequence = customersUK.Contains(customerBSBEV,
  new CustomerComparer());

  Console.WriteLine(isCustomerInSequence? "Yes" : "No");
 } 

Элемент CustomerComparer определяется так:

private class CustomerComparer : IEqualityComparer<Customers>
{
  public bool Equals(Customers x, Customers y) {
  if (x == null || y == null)
  return false;
  return x.CustomerID.Equals(y.CustomerID);
  }

  ...

Выводы

Существует множество стандартных операторов, используемых в качестве методов расширения классов последовательностей Enumerable и Queryable. Они позволяют расширить возможности LINQ. Мы наглядно продемонстрировали, насколько большое количество средств, появившихся в платформе .NET Framework 3.5 (в их числе лямбда-выражения, LINQ, Entity Framework, неявно типизированные переменные), помогает повысить надежность кода и упростить логику приложения.

Джон Папа (John Papa) — старший консультант по технологии .NET в компании ASPSOFT (aspsoft.com) и страстный поклонник бейсбола, который почти все летние вечера проводит, болея за «Янки» со своим семейством и верным псом Кади. Джон, имея звание MVP по C#, является автором нескольких книг технологиям доступа к данным. Он также является спикерм INETA.