Стандартные операторы запроса с LINQ
ОГЛАВЛЕНИЕ
Кроме строго типизированного синтаксиса запросы 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.