Общие сведения о запросах LINQ в C#

Запрос представляет собой выражение, извлекающее данные из источника данных. Различные источники данных имеют разные собственные языки запросов, например SQL для реляционных баз данных и XQuery для XML. Разработчики должны узнать новый язык запросов для каждого типа источника данных или формата данных, который они должны поддерживать. LINQ упрощает эту ситуацию, предлагая согласованную языковую модель C# для типов источников данных и форматов. В запросе LINQ всегда работает с объектами C#. Для запроса и преобразования данных в XML-документах, базах данных SQL, коллекциях .NET и любом другом формате при наличии поставщика LINQ используются те же базовые шаблоны программирования.

Три составляющие операции запроса

Все операции запросов LINQ состоят из трех отдельных действий:

  1. получение источника данных;
  2. создание запроса;
  3. Выполните запрос.

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

// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

// 3. Query execution.
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

На следующем рисунке показана полная операция запроса. Выполнение запроса в LINQ отличается от самого запроса. Другими словами, вы не извлекаете данные, создавая переменную запроса.

Схема завершенной операции запроса LINQ.

Источник данных

Источник данных в предыдущем примере представляет собой массив, поддерживающий универсальный IEnumerable<T> интерфейс. Это значит, что его можно запросить с помощью LINQ. Запрос выполняется в операторе foreach, и для foreach требуется IEnumerable или IEnumerable<T>. Типы, которые поддерживают IEnumerable<T> или производный интерфейс, например универсальный интерфейс IQueryable<T>, называются запрашиваемыми типами.

Запрашиваемый тип не требует внесения изменений или специальной обработки, чтобы служить источником данных LINQ. Если исходные данные еще не в памяти в качестве запрашиваемого типа, поставщик LINQ должен представлять его как таковое. Например, LINQ to XML загружает XML-документ в запрашиваемый тип XElement:

// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");

С помощью EntityFramework создается реляционное сопоставление объектов между классами C# и схемой базы данных. Запросы к объектам записываются, а во время выполнения EntityFramework обрабатывает взаимодействие с базой данных. В следующем примере Customers представляет собой определенную таблицу в базе данных, а тип результата запроса (IQueryable<T>) является производным от IEnumerable<T>.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

// Query for customers in London.
IQueryable<Customer> custQuery =
    from cust in db.Customers
    where cust.City == "London"
    select cust;

Дополнительные сведения о способах создания определенных типов источников данных см. в документации для различных поставщиков LINQ. Однако базовое правило просто: источник данных LINQ — это любой объект, поддерживающий универсальный IEnumerable<T> интерфейс, или интерфейс, наследуемый от него, обычно IQueryable<T>.

Примечание.

Такие типы как ArrayList, которые поддерживают неуниверсальный интерфейс IEnumerable, также можно использовать в качестве источника данных LINQ. Дополнительные сведения см. в разделе Практическое руководство. Выполнение запроса к ArrayList с помощью LINQ (C#).

Запрос

Запрос указывает, какую информацию нужно извлечь из источника или источников данных. При необходимости запрос также указывает, как следует отсортировать, сгруппировать и сформировать данные перед возвратом. Запрос хранится в переменной запроса и инициализируется выражением запроса. Для записи запросов используется синтаксис запросов C#.

В предыдущем примере запрос возвращает все четные числа из массива целых чисел. Выражение запроса содержит три предложения: from, whereи select. (Если вы знакомы с SQL, вы заметили, что порядок предложений отменяется от порядка в SQL.) Предложение from указывает источник данных, where предложение применяет фильтр, а select предложение указывает тип возвращаемых элементов. Все предложения запросов подробно рассматриваются в этом разделе. А сейчас важно то, что в LINQ сама переменная запроса не выполняет никаких действий и не возвращает никаких данных. Она просто хранит сведения, необходимые для предоставления результатов при выполнении запроса на более позднем этапе. Дополнительные сведения о создании запросов см. в разделе "Стандартные операторы запросов" (C#).

Примечание.

Запросы могут также выражаться с помощью синтаксиса методов. Дополнительные сведения см. в разделе Синтаксис запросов и синтаксис методов в LINQ.

Классификация стандартных операторов запросов по способу выполнения

Реализации стандартных методов оператора запроса LINQ to Objects выполняются одним из двух основных способов: немедленной или отложенной. Операторы запросов, использующие отложенное выполнение, можно также разделить на две категории: потоковую передачу и непоток.

Интерпретация

Немедленное выполнение означает, что источник данных считывается и операция выполняется один раз. Все стандартные операторы запросов, возвращающие скалярный результат, выполняются немедленно. Примерами таких запросов являются Count, Max, Average и First. Эти методы выполняются без явной foreach инструкции, так как сам запрос должен использовать foreach для возврата результата. Эти запросы возвращают одно значение, а не коллекцию IEnumerable . Вы можете принудительно выполнить любой запрос немедленно с помощью Enumerable.ToList или Enumerable.ToArray методов. Немедленное выполнение обеспечивает повторное использование результатов запроса, а не объявление запроса. Результаты извлекаются один раз, а затем хранятся для дальнейшего использования. Следующий запрос возвращает количество четных чисел в исходном массиве:

var evenNumQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

int evenNumCount = evenNumQuery.Count();

Чтобы принудительно вызвать немедленное выполнение любого запроса и кэшировать его результаты, вы можете вызвать методы ToList или ToArray.

List<int> numQuery2 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToArray();

Принудительно вызвать выполнение также можно, поместив цикл foreach сразу после выражения запроса. При этом путем вызова ToList или ToArray можно также кэшировать все данные в одном объекте коллекции.

Действие отложено

Отложенное выполнение означает, что операция не выполняется в точке кода, в котором объявлен запрос. Она выполняется только после перечисления переменной запроса, например с помощью оператора foreach. Результаты выполнения запроса зависят от содержимого источника данных при выполнении запроса, а не при определении запроса. Если переменная запроса перечисляется несколько раз, результаты могут каждый раз отличаться. Практически все стандартные операторы запроса, которые возвращают значения типа IEnumerable<T> или IOrderedEnumerable<TElement>, выполняются отложенным способом. Отложенное выполнение обеспечивает повторное использование запроса, так как запрос получает обновленные данные из источника данных при каждом итерации результатов запроса. В следующем коде показан пример отложенного выполнения:

foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

Оператор foreach также является частью кода, в которой извлекаются результаты запроса. Например, в предыдущем запросе переменная итерации num содержит каждое значение (по одному за раз) в возвращенной последовательности.

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

Операторы запросов, использующие отложенное выполнение, можно дополнительно классифицировать как потоковую или непотоковую.

Потоковая передача

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

Непотокирование

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

Таблица классификации

В следующей таблице приведена классификация всех методов стандартных операторов запросов по способу выполнения.

Примечание.

Если оператор помечен в двух столбцах, в операции участвуют две входные последовательности, и каждая последовательность вычисляется по-разному. В таких случаях первая последовательность в списке параметров всегда вычисляется отложенным потоковым способом.

Стандартный оператор запроса Возвращаемый тип Немедленное выполнение Отложенное выполнение потоковой передачи Отложенное выполнение непотоков
Aggregate TSource X
All Boolean X
Any Boolean X
AsEnumerable IEnumerable<T> X
Average Одно числовое значение X
Cast IEnumerable<T> X
Concat IEnumerable<T> X
Contains Boolean X
Count Int32 X
DefaultIfEmpty IEnumerable<T> X
Distinct IEnumerable<T> X
ElementAt TSource X
ElementAtOrDefault TSource? X
Empty IEnumerable<T> X
Except IEnumerable<T> X X
First TSource X
FirstOrDefault TSource? X
GroupBy IEnumerable<T> X
GroupJoin IEnumerable<T> X X
Intersect IEnumerable<T> X X
Join IEnumerable<T> X X
Last TSource X
LastOrDefault TSource? X
LongCount Int64 X
Max Одно числовое значение, TSourceили TResult? X
Min Одно числовое значение, TSourceили TResult? X
OfType IEnumerable<T> X
OrderBy IOrderedEnumerable<TElement> X
OrderByDescending IOrderedEnumerable<TElement> X
Range IEnumerable<T> X
Repeat IEnumerable<T> X
Reverse IEnumerable<T> X
Select IEnumerable<T> X
SelectMany IEnumerable<T> X
SequenceEqual Boolean X
Single TSource X
SingleOrDefault TSource? X
Skip IEnumerable<T> X
SkipWhile IEnumerable<T> X
Sum Одно числовое значение X
Take IEnumerable<T> X
TakeWhile IEnumerable<T> X
ThenBy IOrderedEnumerable<TElement> X
ThenByDescending IOrderedEnumerable<TElement> X
ToArray TSource[] массив X
ToDictionary Dictionary<TKey,TValue> X
ToList IList<T> X
ToLookup ILookup<TKey,TElement> X
Union IEnumerable<T> X
Where IEnumerable<T> X

LINQ to Objects (C#)

"LINQ to Objects" ссылается на использование запросов LINQ с любым IEnumerable или IEnumerable<T> коллекцией напрямую. Вы можете использовать LINQ для запроса любых перечисляемых коллекций, таких как List<T>, Arrayили Dictionary<TKey,TValue>. Коллекция может быть определяемой пользователем или типом, возвращаемым API .NET. При использовании LINQ пишется декларативный код, описывающий, какие данные необходимо извлечь. LINQ to Objects предоставляет отличные общие сведения о программировании с помощью LINQ.

Запросы LINQ предлагают три основных преимущества по сравнению с традиционными foreach циклами:

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

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

Сохранение результатов запроса в памяти

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

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

List<int> numbers = [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20];

IEnumerable<int> queryFactorsOfFour =
    from num in numbers
    where num % 4 == 0
    select num;

// Store the results in a new variable
// without executing a foreach loop.
var factorsofFourList = queryFactorsOfFour.ToList();

// Read and write from the newly created list to demonstrate that it holds data.
Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);

См. также