Поделиться через


LINQ: запрос Language-Integrated .NET

 

Дон Бокс, Андерс Хейлсберг

Февраль 2007 г.

Область применения:
   имя Visual Studio Code "Косатки"
   .NET Framework 3.5

Сводка: Средства запросов общего назначения, добавленные в платформа .NET Framework применяются ко всем источникам информации, а не только к реляционным или XML-данным. Эта возможность называется запросом Language-Integrated .NET (LINQ). (32 печатных страницы)

Содержимое

Запрос Language-Integrated .NET
начало работы со стандартными операторами запросов
Языковые функции, поддерживающие проект LINQ
Дополнительные стандартные операторы запросов
Синтаксис запроса
LINQ to SQL. Интеграция SQL
LINQ to XML: интеграция XML
Сводка

Запрос Language-Integrated .NET

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

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

Мы используем термин "запрос, интегрированный с языком ", чтобы указать, что запрос является интегрированной функцией основных языков программирования разработчика (например, Visual C#, Visual Basic). Интегрированные с языком запросы позволяют использовать расширенные метаданные, проверку синтаксиса во время компиляции, статическую типизацию и Технологию IntelliSense, которые ранее были доступны только для императивного кода. Интегрированные с языком запросы также позволяют применять один декларативный запрос общего назначения ко всей информации в памяти, а не только к информации из внешних источников.

.NET Language-Integrated Query определяет набор стандартных операторов запросов общего назначения, которые позволяют выполнять операции обхода, фильтрации и проекции прямым, но декларативным способом в любом . Язык программирования на основе NET. Стандартные операторы запросов позволяют применять запросы к любому источнику информации на основе IEnumerable<T>. LINQ позволяет третьим сторонам дополнять набор стандартных операторов запросов новыми операторами для конкретного домена, которые подходят для целевой области или технологии. Что еще более важно, сторонние разработчики также могут заменить стандартные операторы запросов собственными реализациями, которые предоставляют дополнительные службы, такие как удаленная оценка, перевод запросов, оптимизация и т. д. Придерживаясь соглашений шаблона LINQ, такие реализации пользуются той же интеграцией языка и поддержкой инструментов, что и стандартные операторы запросов.

Расширяемость архитектуры запросов используется в самом проекте LINQ для предоставления реализаций, работающих с данными XML и SQL. Операторы запросов через XML (LINQ to XML) используют эффективное, простое в использовании средство XML в памяти для предоставления функциональных возможностей XPath/XQuery на языке программирования ведущего приложения. Операторы запросов к реляционным данным (LINQ to SQL) основаны на интеграции определений схем на основе SQL в систему типов среды CLR. Такая интеграция обеспечивает строгую типизацию реляционных данных, сохраняя при этом выразительность реляционной модели и производительность оценки запросов непосредственно в базовом хранилище.

начало работы со стандартными операторами запросов

Чтобы увидеть, как работает запрос, интегрированный с языком, мы начнем с простой программы C# 3.0, которая использует стандартные операторы запросов для обработки содержимого массива:

using System;
using System.Linq;
using System.Collections.Generic;

class app {
  static void Main() {
    string[] names = { "Burke", "Connor", "Frank", 
                       "Everett", "Albert", "George", 
                       "Harris", "David" };

    IEnumerable<string> query = from s in names 
                               where s.Length == 5
                               orderby s
                               select s.ToUpper();

    foreach (string item in query)
      Console.WriteLine(item);
  }
}

Если бы вы скомпилировали и запускали эту программу, вы увидите ее в качестве выходных данных:

BURKE
DAVID
FRANK
To understand how language-integrated query works, we need to dissect the
 first statement of our program.
IEnumerable<string> query = from s in names 
                           where s.Length == 5
                           orderby s
                           select s.ToUpper();

Запрос локальной переменной инициализируется выражением запроса. Выражение запроса работает с одним или несколькими источниками информации, применяя один или несколько операторов запроса из стандартных операторов запросов или операторов, относящихся к предметной области. Это выражение использует три стандартных оператора запроса: Where, OrderBy и Select.

Visual Basic 9.0 также поддерживает LINQ. Ниже приведена предыдущая инструкция, написанная в Visual Basic 9.0:

Dim query As IEnumerable(Of String) = From s in names _
                                     Where s.Length = 5 _
                   Order By s _
                   Select s.ToUpper()

Операторы C# и Visual Basic, показанные здесь, используют выражения запросов. Как и оператор foreach , выражения запросов являются удобным декларативным сокращением кода, который можно написать вручную. Приведенные выше операторы семантически идентичны следующему явному синтаксису, показанному в C#:

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

Эта форма запроса называется запросом на основе метода . Аргументы операторов Where, OrderBy и Select называются лямбда-выражениями, которые являются фрагментами кода так же, как делегаты. Они позволяют определять стандартные операторы запросов по отдельности как методы и объединять их с помощью точечной нотации. Вместе эти методы формируют основу для расширяемого языка запросов.

Языковые функции, поддерживающие проект LINQ

LINQ полностью основан на языковых функциях общего назначения, некоторые из которых являются новыми для C# 3.0 и Visual Basic 9.0. Каждая из этих функций имеет собственную служебную программу, но в совокупности они предоставляют расширяемый способ определения запросов и запрашиваемых API. В этом разделе мы рассмотрим эти языковые функции и их вклад в более прямой и декларативный стиль запросов.

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

Многие операторы запросов позволяют пользователю предоставить функцию, которая выполняет фильтрацию, проекцию или извлечение ключей. Средства запросов опирались на концепцию лямбда-выражений, которые предоставляют разработчикам удобный способ написания функций, которые можно передать в качестве аргументов для последующей оценки. Лямбда-выражения похожи на делегаты СРЕДЫ CLR и должны соответствовать сигнатуре метода, определенной типом делегата. Чтобы проиллюстрировать это, можно развернуть приведенный выше оператор в эквивалентную, но более явную форму, используя тип делегата Func :

Func<string, bool>   filter  = s => s.Length == 5;
Func<string, string> extract = s => s;
Func<string, string> project = s => s.ToUpper();

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

Лямбда-выражения — это естественная эволюция анонимных методов в C# 2.0. Например, мы могли бы написать предыдущий пример с помощью анонимных методов, таких как:

Func<string, bool>   filter  = delegate (string s) {
                                   return s.Length == 5; 
                               };

Func<string, string> extract = delegate (string s) { 
                                   return s; 
                               };

Func<string, string> project = delegate (string s) {
                                   return s.ToUpper(); 
                               };

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

Как правило, разработчик может использовать именованные методы, анонимные методы или лямбда-выражения с операторами запроса. Преимущество лямбда-выражений заключается в том, что они предоставляют наиболее прямой и компактный синтаксис для разработки. Что еще более важно, лямбда-выражения можно компилировать как код или данные, что позволяет обрабатывать лямбда-выражения во время выполнения оптимизаторами, переводчиками и вычислителями.

Пространство имен System.Linq.Expressions определяет различающийся универсальный тип Expression<T>, который указывает, что дерево выражений требуется для данного лямбда-выражения, а не для традиционного тела метода на основе IL. Деревья выражений являются эффективными представлениями лямбда-выражений в памяти и делают структуру выражения прозрачной и явной.

Определение того, будет ли компилятор выдавать исполняемый IL или дерево выражений, определяется способом использования лямбда-выражения. Когда лямбда-выражение назначается переменной, полю или параметру, типом которых является делегат, компилятор выдает IL, идентичный значению анонимного метода. Если лямбда-выражение назначается переменной, полю или параметру с типом Expression<T> для некоторых типов делегатов T, компилятор создает дерево выражений.

Например, рассмотрим следующие два объявления переменных:

Func<int, bool>             f = n => n < 5;
Expression<Func<int, bool>> e = n => n < 5;

Переменная f — это ссылка на делегат, который является исполняемым напрямую:

bool isSmall = f(2); // isSmall is now true

Переменная e является ссылкой на дерево выражений, которое не является исполняемым напрямую:

bool isSmall = e(2); // compile error, expressions == data

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

Expression<Func<int, bool>> filter = n => n < 5;

BinaryExpression    body  = (BinaryExpression)filter.Body;
ParameterExpression left  = (ParameterExpression)body.Left;
ConstantExpression  right = (ConstantExpression)body.Right;

Console.WriteLine("{0} {1} {2}", 
                  left.Name, body.NodeType, right.Value);

В приведенном выше примере дерево выражений разлагается во время выполнения и выводится следующая строка:

n LessThan 5

Эта возможность обрабатывать выражения как данные во время выполнения крайне важна для создания экосистемы сторонних библиотек, использующих базовые абстракции запросов, которые являются частью платформы. Реализация LINQ to SQL доступа к данным использует эту возможность для преобразования деревьев выражений в инструкции T-SQL, подходящие для оценки в хранилище.

Методы расширения

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

Методы расширения определяются в статических классах как статические методы, но помечены атрибутом [System.Runtime.CompilerServices.Extension] в метаданных СРЕДЫ CLR. Языкам рекомендуется предоставлять прямой синтаксис для методов расширения. В C# методы расширения указываются модификатором , который должен применяться к первому параметру метода расширения. Рассмотрим определение простейших операторов запроса Where:

namespace System.Linq {
  using System;
  using System.Collections.Generic;

  public static class Enumerable {
    public static IEnumerable<T> Where<T>(
             this IEnumerable<T> source,
             Func<T, bool> predicate) {

      foreach (T item in source)
        if (predicate(item))
          yield return item;
    }
  }
}

Тип первого параметра метода расширения указывает, к какому типу применяется расширение. В приведенном выше примере метод расширения Where расширяет тип IEnumerable<T>. Поскольку Where — статический метод, мы можем вызывать его напрямую так же, как и любой другой статический метод:

IEnumerable<string> query = Enumerable.Where(names, 
                                          s => s.Length < 6);

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

IEnumerable<string> query = names.Where(s => s.Length < 6);

Методы расширения разрешаются во время компиляции в зависимости от того, какие методы расширения находятся в область. При импорте пространства имен с помощью оператора using в C# или оператора Import в Visual Basic все методы расширения, определенные статическими классами из этого пространства имен, переносятся в область.

Стандартные операторы запросов определяются как методы расширения в типе System.Linq.Enumerable. При изучении стандартных операторов запросов вы заметите, что все, кроме некоторых из них, определены с точки зрения интерфейса IEnumerable<T> . Это означает, что каждый источник информации, совместимый с IEnumerable<T>, получает стандартные операторы запроса, просто добавив следующую инструкцию using в C#:

using System.Linq; // makes query operators visible

Пользователи, которым требуется заменить стандартные операторы запросов для определенного типа, могут либо определить собственные методы с одинаковыми именами для определенного типа с совместимыми сигнатурами, либо определить новые методы расширения с одинаковыми именами, расширяющие определенный тип. Пользователи, которые хотят отказаться от стандартных операторов запросов, могут просто не помещать System.Linq в область и писать собственные методы расширения для IEnumerable<T>.

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

public class MySequence : IEnumerable<int> {
  public IEnumerator<int> GetEnumerator() {
    for (int i = 1; i <= 10; i++) 
      yield return i; 
  }

  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator(); 
  }

  public IEnumerable<int> Where(Func<int, bool> filter) {
    for (int i = 1; i <= 10; i++) 
      if (filter(i)) 
        yield return i;
  }
}

Учитывая это определение класса, следующая программа будет использовать реализацию MySequence.Where , а не метод расширения, так как методы экземпляра имеют приоритет над методами расширения:

MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
    Console.WriteLine(item);

Оператор OfType является одним из немногих стандартных операторов запроса, который не расширяет источник информации на основе IEnumerable<T>. Рассмотрим оператор запроса OfType :

public static IEnumerable<T> OfType<T>(this IEnumerable source) {
  foreach (object item in source) 
    if (item is T) 
      yield return (T)item;
}

OfType принимает не только источники на основе IEnumerable<T>, но и источники, написанные для непараметровизованного интерфейса IEnumerable, который присутствовал в версии 1.0 платформа .NET Framework. Оператор OfType позволяет пользователям применять стандартные операторы запросов к классическим коллекциям .NET следующим образом:

// "classic" cannot be used directly with query operators
IEnumerable classic = new OlderCollectionType();

// "modern" can be used directly with query operators
IEnumerable<object> modern = classic.OfType<object>();

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

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

object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();

При перечислении переменной justStrings в операторе foreach мы получим последовательность из двух строк: Hello и World.

Отложенное вычисление запроса

Наблюдатели могли заметить, что стандартный оператор Where реализуется с помощью конструкции выходов , представленной в C# 2.0. Этот метод реализации является общим для всех стандартных операторов, возвращающих последовательности значений. Интересное преимущество заключается в том, что запрос фактически не вычисляется, пока он не будет перемещен с помощью оператора foreach или вручную с помощью базовых методов GetEnumerator и MoveNext . Эта отложенная оценка позволяет хранить запросы в виде значений на основе T> IEnumerable<, которые можно вычислять несколько раз, каждый раз возвращая потенциально разные результаты.

Для многих приложений это именно то поведение, которое нужно. Для приложений, которые хотят кэшировать результаты оценки запроса, предоставляются два оператора, ToList и ToArray, которые принудительно выполняют немедленную оценку запроса и возвращают список<T> или массив, содержащий результаты оценки запроса.

Чтобы увидеть, как работает отложенная оценка запросов, рассмотрим эту программу, которая выполняет простой запрос к массиву:

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents a query
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');

// evaluate the query
foreach (string item in ayes) 
  Console.WriteLine(item);

// modify the original information source
names[0] = "Bob";

// evaluate the query again, this time no "Allen"
foreach (string item in ayes) 
    Console.WriteLine(item);

Запрос вычисляется при каждом переборе переменной ayes . Чтобы указать, что требуется кэшированная копия результатов, можно просто добавить оператор ToList или ToArray в запрос следующим образом:

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents the result
// of an immediate query evaluation
string[] ayes = names.Where(s => s[0] == 'A').ToArray();

// iterate over the cached query results
foreach (string item in ayes) 
    Console.WriteLine(item);

// modifying the original source has no effect on ayes
names[0] = "Bob";

// iterate over result again, which still contains "Allen"
foreach (string item in ayes)
    Console.WriteLine(item);

И ToArray , и ToList принудительно выполняет немедленную оценку запроса. То же самое относится и к стандартным операторам запросов, возвращающим одноэлементные значения (например, First, ElementAt, Sum, Average, All, Any).

Интерфейс IQueryable<T>

Такая же модель отложенного выполнения обычно является желаемой для источников данных, реализующих функциональность запросов с помощью деревьев выражений, таких как LINQ to SQL. Эти источники данных могут воспользоваться преимуществами реализации интерфейса IQueryable<T> , для которого все операторы запросов, необходимые для шаблона LINQ, реализуются с помощью деревьев выражений. Каждый IQueryable<T> имеет представление "код, необходимый для выполнения запроса" в виде дерева выражений. Все отложенные операторы запросов возвращают новый IQueryable<T> , который дополняет это дерево выражений представлением вызова этого оператора запроса. Таким образом, когда наступает время для оценки запроса, как правило, из-за перечисления IQueryable<T> источник данных может обработать дерево выражения, представляющее весь запрос в одном пакете. Например, сложный запрос LINQ to SQL, полученный с помощью многочисленных вызовов операторов запросов, может привести к отправке в базу данных только одного SQL-запроса.

Преимущество повторного использования этой функции отсрочки путем реализации интерфейса IQueryable<T> для разработчиков источников данных очевидно. С другой стороны, для клиентов, которые пишут запросы, имеет большое преимущество иметь общий тип для удаленных источников информации. Это не только позволяет им создавать полиморфные запросы, которые можно использовать к разным источникам данных, но и открывает возможность написания запросов, которые проходят между доменами.

Инициализация составных значений

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

public class Person {
  string name;
  int age;
  bool canCode;

  public string Name {
    get { return name; } set { name = value; }
  }

  public int Age {
    get { return age; } set { age = value; }
  }

  public bool CanCode {
    get { return canCode; } set { canCode = value; }
  }
}

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

Person value = new Person {
    Name = "Chris Smith", Age = 31, CanCode = false
};

Семантически этот оператор эквивалентен следующей последовательности операторов:

Person value = new Person();
value.Name = "Chris Smith";
value.Age = 31;
value.CanCode = false;

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

IEnumerable<Person> query = names.Select(s => new Person {
    Name = s, Age = 21, CanCode = s.Length == 5
});

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

static Person[] people = {
  new Person { Name="Allen Frances", Age=11, CanCode=false },
  new Person { Name="Burke Madison", Age=50, CanCode=true },
  new Person { Name="Connor Morgan", Age=59, CanCode=false },
  new Person { Name="David Charles", Age=33, CanCode=true },
  new Person { Name="Everett Frank", Age=16, CanCode=true },
};

Структурированные значения и типы

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

В C# синтаксис анонимных типов аналогичен синтаксису инициализации объектов, за исключением того, что имя типа опущено. Например, рассмотрим следующие два оператора:

object v1 = new Person {
    Name = "Brian Smith", Age = 31, CanCode = false
};

object v2 = new { // note the omission of type name
    Name = "Brian Smith", Age = 31, CanCode = false
};

Переменные версии 1 и 2 указывают на объект в памяти, чей тип CLR имеет три открытых свойства Name, Age и CanCode. Переменные отличаются тем, что версия 2 ссылается на экземпляр анонимного типа. С точки зрения СРЕДЫ CLR анонимные типы ничем не отличаются от любых других типов. Что делает анонимные типы особенными, так это то, что они не имеют понятного имени в вашем языке программирования. Единственный способ создать экземпляры анонимного типа — использовать синтаксис, показанный выше.

Чтобы разрешить переменным ссылаться на экземпляры анонимных типов, но по-прежнему использовать преимущества статической типизации, C# вводит неявно типизированные локальные переменные:вместо имени типа для объявлений локальных переменных можно использовать var ключевое слово. Например, рассмотрим эту юридическую программу C# 3.0:

var s = "Bob";
var n = 32;
var b = true;

Var ключевое слово указывает компилятору определить тип переменной из статического типа выражения, используемого для инициализации переменной. В этом примере типы s, n и bstring, int и bool соответственно. Эта программа идентична следующей:

string s = "Bob";
int    n = 32;
bool   b = true;

Var ключевое слово является удобным для переменных, типы которых имеют значимые имена, но это необходимо для переменных, ссылающихся на экземпляры анонимных типов.

var value = new { 
  Name = " Brian Smith", Age = 31, CanCode = false
};

В приведенном выше примере значение переменной имеет анонимный тип, определение которого эквивалентно следующему псевдо-C#:

internal class ??? {
  string _Name;
  int    _Age;
  bool   _CanCode;

  public string Name { 
    get { return _Name; } set { _Name = value; }
  }

  public int Age{ 
    get { return _Age; } set { _Age = value; }
  }

  public bool CanCode { 
    get { return _CanCode; } set { _CanCode = value; }
  }

  public bool Equals(object obj) { ... }

  public bool GetHashCode() { ... }
}

Анонимные типы нельзя совместно использовать в пределах сборки; однако компилятор гарантирует наличие не более одного анонимного типа для заданной последовательности пар "имя-тип" свойства в каждой сборке.

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

Например, рассмотрим этот пример, который создает новое структурированное значение путем объединения свойств из других значений:

var bob = new Person { Name = "Bob", Age = 51, CanCode = true };
var jane = new { Age = 29, FirstName = "Jane" };

var couple = new {
    Husband = new { bob.Name, bob.Age },
    Wife = new { Name = jane.FirstName, jane.Age }
};

int    ha = couple.Husband.Age; // ha == 51
string wn = couple.Wife.Name;   // wn == "Jane"

Ссылки на поля или свойства, показанные выше, — это просто удобный синтаксис для написания следующей более явной формы:

var couple = new {
    Husband = new { Name = bob.Name, Age = bob.Age },
    Wife = new { Name = jane.FirstName, Age = jane.Age }
};

В обоих случаях переменная пары получает собственную копию свойств Name и Age от Bob and Jane.

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

var query = people.Select(p => new { 
               p.Name, BadCoder = p.Age == 11
           });

foreach (var item in query) 
  Console.WriteLine("{0} is a {1} coder", 
                     item.Name,
                     item.BadCoder ? "bad" : "good");

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

Дополнительные стандартные операторы запросов

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

Сортировка и группирование

Как правило, оценка запроса приводит к последовательности значений, которые создаются в определенном порядке, который является встроенным в базовых источниках информации. Чтобы предоставить разработчикам явный контроль над порядком, в котором создаются эти значения, для управления порядком определяются стандартные операторы запросов. Самым основным из этих операторов является оператор OrderBy .

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

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

// unity sort
var s1 = names.OrderBy(s => s); 
var s2 = names.OrderByDescending(s => s);

// sort by length
var s3 = names.OrderBy(s => s.Length); 
var s4 = names.OrderByDescending(s => s.Length);

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

Чтобы разрешить несколько критериев сортировки, orderBy и OrderByDescending возвращают OrderSequence<T> , а не универсальный IEnumerable<T>. Два оператора определяются только для OrderedSequence<T>, а именно ThenBy и ThenByDescending , которые применяют дополнительный (подчиненный) критерий сортировки. ThenBy/ThenByDescending сами возвращают Значение OrderedSequence<T>, что позволяет применять любое количество операторов ThenBy/ThenByDescending :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);

При оценке запроса, на который ссылается s1 в этом примере, будет получена следующая последовательность значений:

"Burke", "David", "Frank", 
"Albert", "Connor", "George", "Harris", 
"Everett"

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

Оператор OrderBy накладывает порядок сортировки на последовательность значений. Стандартные операторы запросов также включают оператор GroupBy , который применяет секционирование для последовательности значений на основе функции извлечения ключей. Оператор GroupBy возвращает последовательность значений IGrouping , по одному для каждого обнаруженного значения ключа. IGrouping — это IEnumerable, который дополнительно содержит ключ, который использовался для извлечения его содержимого:

public interface IGrouping<K, T> : IEnumerable<T> {
  public K Key { get; }
}

Простейший способ применения GroupBy выглядит следующим образом:

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// group by length
var groups = names.GroupBy(s => s.Length);

foreach (IGrouping<int, string> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (string value in group)
        Console.WriteLine("  {0}", value);
}    

При запуске эта программа выводит следующее:

Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank
Strings of length 7
  Everett

A la Select, GroupBy позволяет предоставить функцию проекции, которая используется для заполнения членов групп.

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// group by length
var groups = names.GroupBy(s => s.Length, s => s[0]);
foreach (IGrouping<int, char> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (char value in group)
        Console.WriteLine("  {0}", value);
}  

В этом варианте выводится следующее:

Strings of length 6
  A
  C
  G
  H
Strings of length 5
  B
  D
  F
Strings of length 7
  E

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

Операторы агрегирования

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

public static U Aggregate<T, U>(this IEnumerable<T> source, 
                                U seed, Func<U, T, U> func) {
  U result = seed;

  foreach (T element in source) 
      result = func(result, element);

  return result;
}

Оператор Aggregate упрощает вычисление последовательности значений. Агрегат работает путем вызова лямбда-выражения один раз для каждого члена базовой последовательности. Каждый раз, когда Aggregate вызывает лямбда-выражение, оно передает как элемент из последовательности, так и агрегированное значение (начальное значение — это начальный параметр в Aggregate). Результат лямбда-выражения заменяет предыдущее агрегированное значение, а aggregate возвращает окончательный результат лямбда-выражения.

Например, эта программа использует aggregate для накопления общего количества символов в массиве строк:

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int count = names.Aggregate(0, (c, s) => c + s.Length);
// count == 46

В дополнение к оператору агрегата общего назначения стандартные операторы запросов также включают оператор count общего назначения и четыре числовых оператора агрегирования (Min, Max, Sum и Average), которые упрощают эти распространенные операции агрегирования. Числовые агрегатные функции работают над последовательности числовых типов (например, int, double, decimal) или над последовательностью произвольных значений, если функция предоставляется для проецирования элементов последовательности в числовой тип.

Эта программа иллюстрирует обе описанные формы оператора Sum :

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int total1 = numbers.Sum();            // total1 == 55
int total2 = names.Sum(s => s.Length); // total2 == 46

Примечание Второй оператор Sum эквивалентен предыдущему примеру, использующим Aggregate.

Выберите и ВыберитеMany

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

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.Select(s => s.Split(' '));

foreach (string[] line in tokens)
    foreach (string token in line)
        Console.Write("{0}.", token);

При запуске эта программа выводит следующий текст:

Albert.was.here.Burke.slept.late.Connor.is.happy.

В идеале мы хотели бы, чтобы наш запрос возвращал объединенную последовательность маркеров и не предоставлял потребителю промежуточную строку[] . Для этого мы используем оператор SelectMany вместо оператора Select . Оператор SelectMany работает аналогично оператору Select . Она отличается тем, что функция преобразования должна возвращать последовательность, которая затем разворачивается с помощью оператора SelectMany . Вот наша программа, перезаписаемая с помощью SelectMany:

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.SelectMany(s => s.Split(' '));

foreach (string token in tokens)
    Console.Write("{0}.", token);

Использование SelectMany приводит к расширению каждой промежуточной последовательности в рамках нормальной оценки.

SelectMany идеально подходит для объединения двух источников информации:

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.SelectMany(n => 
                     people.Where(p => n.Equals(p.Name))
                 );

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

Операторы соединения

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

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

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.Join(people, n => n, p => p.Name, (n,p) => p);

Это немного глоток, но посмотрите, как фрагменты сочетаются друг с другом: метод Join вызывается для "внешнего" источника данных , имен. Первый аргумент — это "внутренний" источник данных, люди. Второй и третий аргументы являются лямбда-выражениями для извлечения ключей из элементов внешних и внутренних источников соответственно. Эти ключи используются методом Join для сопоставления элементов. Здесь мы хотим, чтобы сами имена соответствовали свойству Name людей. Окончательное лямбда-выражение отвечает за создание элементов результирующей последовательности: оно вызывается с каждой парой соответствующих элементов n и p и используется для формирования результата. В этом случае мы решили отменить n и вернуть p. Конечным результатом является список элементов Personлюдей , имя которых находится в списке имен.

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

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.GroupJoin(people, n => n, p => p.Name,                   
                 (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

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

Синтаксис запроса

Существующий оператор foreach в C# предоставляет декларативный синтаксис для итерации по методам IEnumerable/IEnumerator .NET Frameworks. Оператор foreach является строго необязательным, но он оказался очень удобным и популярным языковым механизмом.

Основываясь на этом прецеденте, выражения запросов упрощают запросы с помощью декларативного синтаксиса для наиболее распространенных операторов запросов: Where, Join, GroupJoin, Select, SelectMany, GroupBy, OrderBy, ThenBy, OrderByDescending, ThenByDescending и Cast.

Для начала рассмотрим простой запрос, который мы начали с этого документа:

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

Используя выражение запроса, можно переписать это точное выражение следующим образом:

IEnumerable<string> query = from s in names 
                            where s.Length == 5
                            orderby s
                            select s.ToUpper();

Как и в случае с оператором foreach в C#, выражения запросов являются более компактными и удобными для чтения, но являются полностью необязательными. Каждое выражение, которое может быть записано как выражение запроса, имеет соответствующий (хотя и более подробный) синтаксис с использованием точечной нотации.

Для начала рассмотрим базовую структуру выражения запроса. Каждое синтаксическое выражение запроса в C# начинается с предложения from и заканчивается предложением select или group . За предложением initial from может следовать ноль или более предложений from, let, where, join и orderby . Каждое предложение from является генератором, который вводит переменную диапазона в последовательности; каждое предложение let дает имя результату выражения; и каждое предложение where является фильтром, исключающим элементы из результата. Каждое предложение join сопоставляет новый источник данных с результатами предыдущих предложений. Предложение orderby задает порядок для результата:

query-expression ::= from-clause query-body

query-body ::= 

      query-body-clause* final-query-clause query-continuation?

query-body-clause ::=
 (from-clause 
      | join-clause 
      | let-clause 
      | where-clause 
      | orderby-clause)

from-clause ::=from itemName in srcExpr

join-clause ::=join itemName in srcExpr on keyExpr equals keyExpr 
       (into itemName)?

let-clause ::=let itemName = selExpr

where-clause ::= where predExpr

orderby-clause ::= orderby (keyExpr (ascending | descending)?)*

final-query-clause ::=
 (select-clause | groupby-clause)

select-clause ::= select selExpr

groupby-clause ::= group selExpr by keyExprquery-continuation ::= intoitemName query-body

Например, рассмотрим два выражения запроса:

var query1 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             select new { 
                 p.Name, Senior = p.Age > 30, p.CanCode
             };

var query2 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             group new { 
                p.Name, Senior = p.Age > 30, p.CanCode
             } by p.CanCode;

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

var query1 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .Select(p => new { 
                       p.Name, 
                       Senior = p.Age > 30, 
                       p.CanCode
                   });

var query2 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .GroupBy(p => p.CanCode, 
                            p => new {
                                   p.Name, 
                                   Senior = p.Age > 30, 
                                   p.CanCode
                   });

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

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

var query = from s1 in names 
            where s1.Length == 5
            from s2 in names 
            where s1 == s2
            select s1 + " " + s2;

При выполнении для этого входного массива:

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

Мы получим следующие результаты:

Burke Burke
Frank Frank
David David

Приведенное выше выражение запроса расширяется до следующего выражения точечной нотации:

var query = names.Where(s1 => s1.Length == 5)
                 .SelectMany(s1 => names, (s1,s2) => new {s1,s2})
                 .Where($1 => $1.s1 == $1.s2) 
                 .Select($1 => $1.s1 + " " + $1.s2);

Примечание Эта версия SelectMany принимает дополнительное лямбда-выражение, которое используется для получения результата на основе элементов из внешней и внутренней последовательностей. В этом лямбда-выражении две переменные диапазона собираются в анонимном типе. Компилятор изобретает переменную с именем $1 , чтобы обозначить этот анонимный тип в последующих лямбда-выражениях.

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

var query = from n in names
            join p in people on n equals p.Name into matching
            select new { Name = n, Count = matching.Count() };

Неудивительно, что этот запрос расширяется непосредственно в тот, который мы видели ранее:

var query = names.GroupJoin(people, n => n, p => p.Name,                   
           (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

Часто бывает полезно рассматривать результаты одного запроса как генератор в последующем запросе. Для поддержки этого выражения запроса используют в ключевое слово для спления нового выражения запроса после предложения select или group. Это называется продолжением запроса.

Ключевое слово особенно полезен для постобработки результатов предложения group by. Например, рассмотрим эту программу:

var query = from item in names
            orderby item
            group item by item.Length into lengthGroups
            orderby lengthGroups.Key descending
            select lengthGroups;

foreach (var group in query) { 
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (var val in group)
        Console.WriteLine("  {0}", val);
}

Эта программа выводит следующие выходные данные:

Strings of length 7
  Everett
Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank

В этом разделе было описано, как C# реализует выражения запросов. Другие языки могут поддерживать дополнительные операторы запросов с явным синтаксисом или вообще не использовать выражения запросов.

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

Эта расширяемость используется в самом проекте LINQ путем подготовки двух API с поддержкой LINQ, а именно LINQ to SQL, который реализует шаблон LINQ для доступа к данным на основе SQL, и LINQ to XML который позволяет выполнять запросы LINQ по XML-данным. Оба они описаны в следующих разделах.

LINQ to SQL: интеграция SQL

.NET Language-Integrated Query можно использовать для запроса реляционных хранилищ данных, не выходя из синтаксиса или среды компиляции локального языка программирования. Это средство с кодовой LINQ to SQL использует преимущества интеграции сведений о схеме SQL в метаданные СРЕДЫ CLR. Эта интеграция компилирует определения таблиц SQL и представлений в типы CLR, доступ к которым можно получить с любого языка.

LINQ to SQL определяет два основных атрибута, [Таблица] и [Столбец], которые указывают, какие типы и свойства СРЕДЫ CLR соответствуют внешним данным SQL. Атрибут [Table] можно применить к классу и связать тип СРЕДЫ CLR с именованной таблицей или представлением SQL. Атрибут [Column] может применяться к любому полю или свойству и связывает элемент с именованным столбцом SQL. Оба атрибута параметризуются для сохранения метаданных SQL. Например, рассмотрим это простое определение схемы SQL:

create table People (
    Name nvarchar(32) primary key not null, 
    Age int not null, 
    CanCode bit not null
)

create table Orders (
    OrderID nvarchar(32) primary key not null, 
    Customer nvarchar(32) not null, 
    Amount int
)

Эквивалент среды CLR выглядит следующим образом:

[Table(Name="People")]
public class Person {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string Name; 

  [Column]
  public int Age;

  [Column]
  public bool CanCode;
}

[Table(Name="Orders")]
public class Order {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string OrderID; 

  [Column(DbType="nvarchar(32) not null")]        
  public string Customer; 

  [Column]
  public int? Amount; 
}

Примечание В этом примере столбцы, допускающие значение NULL, сопоставляются с типами, допускающими значение NULL, в среде CLR (типы, допускающие значение NULL впервые появились в версии 2.0 платформа .NET Framework), и что для типов SQL, которые не соответствуют 1:1 типу CLR (например, nvarchar, char, text), исходный тип SQL сохраняется в метаданных СРЕДЫ CLR.

Чтобы выполнить запрос к реляционному хранилищу, LINQ to SQL реализация шаблона LINQ преобразует запрос из формы дерева выражений в выражение SQL и ADO.NET объект DbCommand, подходящий для удаленного вычисления. Например, рассмотрим этот простой запрос:

// establish a query context over ADO.NET sql connection
DataContext context = new DataContext(
     "Initial Catalog=petdb;Integrated Security=sspi");

// grab variables that represent the remote tables that 
// correspond to the Person and Order CLR types
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders   = context.GetTable<Order>();

// build the query
var query = from c in custs
            from o in orders
            where o.Customer == c.Name
            select new { 
                       c.Name, 
                       o.OrderID,
                       o.Amount,
                       c.Age
            }; 

// execute the query
foreach (var item in query) 
    Console.WriteLine("{0} {1} {2} {3}", 
                      item.Name, item.OrderID, 
                      item.Amount, item.Age);

Тип DataContext предоставляет упрощенный переводчик, который преобразует стандартные операторы запросов в SQL. DataContext использует существующий ADO.NET IDbConnection для доступа к хранилищу и может быть инициализирован с помощью установленного объекта подключения ADO.NET или строки подключения, которую можно использовать для его создания.

Метод GetTable предоставляет переменные, совместимые с IEnumerable, которые можно использовать в выражениях запроса для представления удаленной таблицы или представления. Вызовы GetTable не вызывают никакого взаимодействия с базой данных, скорее они представляют потенциальную возможность взаимодействия с удаленной таблицей или представлением с помощью выражений запроса. В приведенном выше примере запрос не передается в хранилище, пока программа не проверит итерацию по выражению запроса, в данном случае с помощью оператора foreach в C#. Когда программа впервые выполняет итерацию по запросу, механизм DataContext преобразует дерево выражений в следующую инструкцию SQL, которая отправляется в хранилище:

SELECT [t0].[Age], [t1].[Amount], 
       [t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]

Важно отметить, что, создавая возможности запросов непосредственно на локальном языке программирования, разработчики получают все возможности реляционной модели без необходимости статически создавать связи в типе CLR. При этом комплексное сопоставление объектов и реляционных данных также может воспользоваться этой основной возможностью запросов для пользователей, которым требуется эта функция. LINQ to SQL предоставляет функции объектно-реляционного сопоставления, с помощью которых разработчик может определять связи между объектами и перемещаться по ним. Вы можете ссылаться на Orders как на свойство класса Customer с помощью сопоставления, чтобы не требуется явных соединений для связывания этих двух элементов. Внешние файлы сопоставления позволяют отделить сопоставление от объектной модели для более широких возможностей сопоставления.

LINQ to XML: интеграция XML

.NET Language-Integrated Query for XML (LINQ to XML) позволяет запрашивать XML-данные с помощью стандартных операторов запросов, а также операторов, зависящих от дерева, которые обеспечивают навигацию по типу XPath через потомков, предков и одноуровневых элементов. Он обеспечивает эффективное представление xml в памяти, которое интегрируется с существующей инфраструктурой чтения и записиSystem.Xmlи проще в использовании, чем W3C DOM. Существует три типа, которые выполняют большую часть работы по интеграции XML с запросами: XName, XElement и XAttribute.

XName предоставляет простой в использовании способ обработки идентификаторов с указанием пространства имен (QNames), используемых в качестве имен элементов и атрибутов. XName прозрачно обрабатывает эффективную атомизацию идентификаторов и позволяет использовать символы или обычные строки везде, где требуется QName.

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

var e = new XElement("Person", 
                     new XAttribute("CanCode", true),
                     new XElement("Name", "Loren David"),
                     new XElement("Age", 31));

var s = e.ToString();

Это соответствует следующему XML-коду:

<Person CanCode="true">
  <Name>Loren David</Name> 
  <Age>31</Age> 
</Person>

Обратите внимание, что для создания XML-выражения не требуется шаблон фабрики на основе модели DOM и что реализация ToString дала текстовый XML-код. Элементы XML также можно создать из существующего XmlReader или строкового литерала:

var e2 = XElement.Load(xmlReader);
var e1 = XElement.Parse(
@"<Person CanCode='true'>
  <Name>Loren David</Name>
  <Age>31</Age>
</Person>");

XElement также поддерживает создание XML с использованием существующего типа XmlWriter .

XElement сопоставляется с операторами запросов, позволяя разработчикам создавать запросы к информации, отличной от XML, и получать результаты XML путем построения XElements в теле предложения select:

var query = from p in people 
            where p.CanCode
            select new XElement("Person", 
                                  new XAttribute("Age", p.Age),
                                  p.Name);

Этот запрос возвращает последовательность XElements. Чтобы разрешить построение XElements на основе результата такого запроса, конструктор XElement позволяет передавать последовательности элементов в качестве аргументов напрямую:

var x = new XElement("People",
                  from p in people 
                  where p.CanCode
                  select 
                    new XElement("Person", 
                                   new XAttribute("Age", p.Age),
                                   p.Name));

Это XML-выражение приводит к следующему XML-коду:

<People>
  <Person Age="11">Allen Frances</Person> 
  <Person Age="59">Connor Morgan</Person> 
</People>

Приведенная выше инструкция имеет прямой перевод в Visual Basic. Однако Visual Basic 9.0 также поддерживает использование XML-литералы, которые позволяют выразить выражения запроса с помощью декларативного синтаксиса XML непосредственно из Visual Basic. Предыдущий пример можно создать с помощью оператора Visual Basic:

 Dim x = _
        <People>
             <%= From p In people __
                 Where p.CanCode _

                 Select <Person Age=<%= p.Age %>>p.Name</Person> _
             %>
        </People>

В примерах до сих пор показано , как создавать новые ЗНАЧЕНИЯ XML с помощью запроса, интегрированного с языком. Типы XElement и XAttribute также упрощают извлечение информации из XML-структур. XElement предоставляет методы доступа, которые позволяют применять выражения запросов к традиционным осям XPath. Например, следующий запрос извлекает только имена из XElement , показанного выше:

IEnumerable<string> justNames =
    from e in x.Descendants("Person")
    select e.Value;

//justNames = ["Allen Frances", "Connor Morgan"]

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

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int)e.Attribute("Age") 
    };

Обратите внимание, что XAttribute и XElement поддерживают явные преобразования для извлечения текстового значения в качестве примитивного типа. Чтобы справиться с отсутствующими данными, можно просто привести к типу, допускающим значение NULL:

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int?)e.Attribute("Age") ?? 21
    };

В этом случае мы используем значение по умолчанию 21 , если отсутствует атрибут Age .

Visual Basic 9.0 обеспечивает прямую языковую поддержку методов доступа Elements, Attribute и DescendantsxElement, что позволяет получить доступ к xml-данным с помощью более компактного и прямого синтаксиса, называемого свойствами оси XML. Эту функцию можно использовать для написания предыдущей инструкции C# следующим образом:

Dim persons = _
      From e In x...<Person> _   
      Select new Person { _
          .Name = e.Value, _
          .Age = IIF(e.@Age, 21) _
      } 

В Visual Basic x...<Пользователь> получает все элементы в коллекции Потомков x с именем Person, а выражение e.@Age находит все XAttributes с именем Age. . Свойство Value получает первый атрибут в коллекции и вызывает свойство Value для этого атрибута.

Сводка

.NET Language-Integrated Query добавляет возможности запросов в среду CLR и языки, предназначенные для нее. Механизм запросов основан на лямбда-выражениях и деревьях выражений, позволяя использовать предикаты, проекции и выражения извлечения ключей в качестве непрозрачного исполняемого кода или как прозрачные данные в памяти, подходящие для обработки или преобразования нижестоящего потока. Стандартные операторы запросов, определенные в проекте LINQ, работают над любым источником информации на основе IEnumerable<T> и интегрированы с ADO.NET (LINQ to SQL) и System.Xml (LINQ to XML), что позволяет реляционным и XML-данным получить преимущества интегрированных с языком запросов.

Стандартные операторы запросов в двух словах

Оператор Описание
Where Оператор ограничения на основе функции предиката
Select/SelectMany Операторы проекции на основе функции селектора
Take/Skip/TakeWhile/SkipTime Операторы секционирования на основе позиции или функции предиката
Присоединение к группе Операторы объединения на основе функций селектора ключей
Concat Concatenation, оператор
OrderBy/ThenBy/OrderByDescending/ThenByDescending Сортировка операторов сортировки в порядке возрастания или убывания на основе необязательных функций выбора ключей и функции сравнения
Reverse Оператор сортировки, отменяющий порядок последовательности
GroupBy Оператор группировки на основе необязательных функций выбора ключей и функции сравнения
Distinct Оператор Set, удаляющий дубликаты
Объединение и пересечение Операторы set, возвращающие объединение или пересечение набора
Исключения Оператор Set, возвращающий разницу в наборе
AsEnumerable Оператор преобразования в IEnumerable<T>
ToArray/ToList Оператор преобразования в массив или list<T>
ToDictionary/ToLookup Операторы преобразования в Dictionary<K,T> или Lookup<K,T> (много словарь) на основе функции селектора ключей
OfType/Cast Операторы преобразования в IEnumerable<T> на основе фильтрации или преобразования в аргумент типа
SequenceEqual Оператор равенства, проверяя равенство парных элементов
First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault Операторы элементов, возвращающие начальный, окончательный или единственный элемент на основе необязательной функции предиката
ElementAt/ElementAtOrDefault Операторы элементов, возвращающие элемент на основе позиции
DefaultIfEmpty Оператор элемента, заменяющий пустую последовательность одноэлементной последовательностью по умолчанию
Диапазон Оператор создания, возвращающий числа в диапазоне
Повторить Оператор создания, возвращающий несколько вхождений заданного значения
Empty Оператор создания, возвращающий пустую последовательность
Любые/все Квантификатор проверки экзистенциального или универсального удовлетворения функции предиката
Содержит Квантификатор, проверяющий наличие заданного элемента
Count/LongCount Агрегатные операторы, подсчитывающие элементы на основе необязательной функции предиката
Сумма,мин.,максимум/среднее Агрегатные операторы на основе необязательных функций селектора
Статистическое Агрегатный оператор, накапливающий несколько значений на основе функции накопления и необязательного начального значения