Введение в запросы 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 отличается от самого запроса. Другими словами, вы не извлекаете данные, создавая переменную запроса.

Diagram of the complete LINQ query operation.

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

Источник данных в предыдущем примере представляет собой массив, поддерживающий универсальный 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.

Выполнение запроса

Отложенное выполнение

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

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

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

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

Принудительное немедленное выполнение

Запросы, выполняющие статистические функции для диапазона исходных элементов, сначала должны выполнить итерацию по этим элементам. Примерами таких запросов являются Count, Max, Average и First. Эти методы выполняются без явной foreach инструкции, так как сам запрос должен использовать foreach для возврата результата. Эти запросы возвращают одно значение, а не коллекцию IEnumerable . Следующий запрос возвращает количество четных чисел в исходном массиве:

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 можно также кэшировать все данные в одном объекте коллекции.

См. также