10 мифов о LINQ

Автор: Джозеф Албахари
Перевод: Гуев Тимур
Опубликовано: 23.04.2014
Версия текста: 1.1
Миф #1
Миф #2
Миф #3
Миф #4
Миф #5
Миф #6
Миф #7
Миф #8
Миф #9
Миф #10

Миф #1

Все LINQ-запросы должны начинаться с ключевого слова 'var'. По сути, основная цель ключевого слова 'var' – начать LINQ запрос!

Ключевое слово var и LINQ-запросы - это самостоятельные концепции. Ключевое слово var позволяет компилятору вывести тип локальной переменной на основании начального присваивания (неявная типизация). К примеру, следующий код:

      var s = "Hello"; 

точный эквивалент для:

      string s = "Hello"; 

потому что компилятор выводит тип переменной s как string.

Аналогично, следующий запрос:

string[] people = new [] { "Tom", "Dick", "Harry" };
var filteredPeople = people.Where (p => p.Length > 3); 

точный эквивалент для:

string[] people = new [] { "Tom", "Dick", "Harry" };
IEnumerable<string> filteredPeople = people.Where (p => p.Length > 3); 

Можно заметить, что всё, чего мы добились с помощью ключевого слова var – это создали сокращение для IEnumerable<string>. Многие люди любят такую запись, поскольку она короче; другие же считают, что неявная типизация способна сделать код менее понятным.

Бывают ситуации, в которых для LINQ-запросов необходимо ключевое слово var. Это происходит при проекции в анонимный тип:

string[] people = new [] { "Tom", "Dick", "Harry" };
var filteredPeople = people.Select (p => new { Name = p, p.Length }); 

Следующий пример показывает, как использовать анонимный тип вне LINQ-контекста:

      var person = new { Name="Foo", Length=3 };

Миф #2

Все LINQ-запросы должны использовать синтаксис запросов.

Существует два способа записи LINQ запросов: лямбда-синтаксис и синтаксис запросов. Пример лямбда-синтаксиса:

string[] people = new [] { "Tom", "Dick", "Harry" };
var filteredPeople = people.Where (p => p.Length > 3); 

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

string[] people = new [] { "Tom", "Dick", "Harry" };
var filteredPeople = from p in people where p.Length > 3 select p; 

Логически, компилятор транслирует синтаксис запросов в лямбда-синтаксис. Это означает, что всё, что может быть выражено с помощью синтаксиса запросов, также может быть выражено в лямбда-синтаксисе. Синтаксис запросов может быть намного проще в случае запросов, включающих более одной переменной диапазона (в данном примере мы использовали только одну переменную диапазона p, так что оба запроса выглядят одинаково просто).

Не все операторы доступны в синтаксисе запросов, так что эти два вида синтаксиса скорее дополняют друг друга. Чтобы получить лучшее от каждого, вы можете смешивать стили запросов в одном выражении (см. миф # 5).

Миф #3

Чтобы извлечь всех клиентов из таблицы, вы должны использовать запрос наподобие этого: var query = from c in db.Customers select c.

Выражение
The expression:
from c in db.Customers select c 

является слишком многословным! Вы можете просто использовать:

db.Customers

Аналогично, следующий LINQ to XML запрос:

      var xe = from e in myXDocument.Descendants ("phone") select e;

может быть упрощен до:

      var xe = myXDocument.Descendants ("phone");
И этот запрос:
Customer customer = (from c in db.Customers where c.ID == 123 select c)
                    .Single();

можно упростить до:

Customer customer = db.Customers.Single (c => c.ID == 123);

Миф #4

Чтобы воспроизвести SQL-запрос в LINQ, вы должны сделать LINQ-запрос максимально похожим на SQL-запрос.

LINQ и SQL - разные языки, использующие разные концепции.

Возможно, главным барьером продуктивного использования LINQ является синдром "думать в терминах SQL": мысленно представлять запрос на языке SQL, а затем переводить его на LINQ. Результатом будет постоянная борьба с API!

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

Миф #5

Для эффективного соединения в LINQ, вы должны использовать ключевое слово join.

Это правда, но только для запросов к локальным коллекциям. Когда вы создаете запрос к базе данных, ключевое слово join вовсе необязательно: операция соединения может быть заменена использованием нескольких from и подзапросов. Несколько from и подзапросы являются более универсальными: вы также можете реализовать соединение не по равенству (non-equi-join).

Более того в LINQ to SQL и Entity Framework вы можете запрашивать свойства ассоциации, уменьшающие потребность в join-ах! К примеру, следующий код показывает, как можно извлечь имена и идентификаторы всех клиентов, не совершивших ни одной покупки:

from c in db.Customers
where !c.Purchases.Any()
select new { c.ID, c.Name } 

Или выбрать клиентов, не совершавших покупок на сумму свыше $1000:

from c in db.Customers
where !c.Purchases.Any (p => p.Price > 1000)
select new { c.ID, c.Name } 

Заметьте, мы смешали лямбда-синтаксис и синтаксис запросов. Большее количество примеров свойств ассоциации, руководства по соединениям и смешанного синтаксиса смотри на LINQPad.

Миф #6

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

Это следствие из Мифа #4. Одно из основных преимуществ LINQ заключается в том, что вы можете:

  1. Запрашивать структурированный объект через свойства ассоциации (вместо того чтобы соединяться вручную);
  2. Проецировать прямо в иерархию объектов.

В принципе, 1 и 2 независимы, однако 1 помогает 2. К примеру, если вы хотите извлечь номера клиентов в штате WA вместе с их покупками, вы можете использовать следующий код:

from c in db.Customers
where c.State == "WA"
select new
{
   c.Name,
   c.Purchases    // An EntitySet (collection)
}

Иерархический результат этого запроса намного проще в работе, чем плоский набор данных.

Можно достигнуть такого же результата, не используя свойства ассоциаций:

from c in db.Customers
where c.State == "WA"
select new
{
   c.Name,
   Purchases = db.Purchases.Where (p => p.CustomerID == c.ID)
}

Миф #7

Чтобы реализовать внешнее соединение в LINQ to SQL вы всегда должны использовать оператор DefaultIfEmpty().

Это правда, если вам нужен плоский набор данных. Пример из предыдущего мифа транслируется в левое внешнее соединение (left outer join) в SQL и не требует оператора DefaultIfEmpty.

Миф #8

Запросы LINQ to SQL или Entity Framework будут выполняться как единое целое, только если они были построены в один шаг.

LINQ использует отложенную модель выполнения, то есть запросы выполняются не во время создания, а во время перечисления. Это означает, что вы можете конструировать ваши запросы во столько шагов, во сколько захотите, и они не попадут на сервер до тех пор, пока вы не начнёте использовать результат.

К примеру, следующий запрос извлекает имена всех клиентов, совершивших две покупки, чьё имя начинается с символа 'А'. Этот запрос можно построить в три шага:

var query = db.Customers.Where (c => c.Name.StartsWith ("A"));
query = query.Where (c => c.Purchases.Count() >= 2);
var result = query.Select (c => c.Name);

foreach (string name in result)   // Только сейчас запрос начинает выполняться!
   Console.WriteLine (name);

Миф #9

Метод не может вернуть запрос, если он заканчивается оператором ‘new’.

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

public IQueryable<NameDetails> GetCustomerNamesInState (string state)
{
   return
      from c in Customer
      where c.State == state
      select new NameDetails
      {
         FirstName = c.FirstName,
         LastName = c.LastName
      };
}

Класс NameDetails определен следующим образом:

public class NameDetails
{
   public string FirstName, LastName;
}

Миф #10

Лучший способ использования LINQ to SQL – это создать единственный экземпляр класса DataContext в статическом свойстве и использовать этот общий экземпляр на протяжении всей жизни приложения.

Такая стратегия приведет к устаревшим данным, поскольку объекты, отслеживаемые экземпляром DataContext, не обновляются при повторном запросе.

Используя единственный экземпляр класса DataContext, вы получите много неприятностей, поскольку он не является потокобезопастным.

Правильной стратегией является создание нового экземпляра DataContext по каждому запросу объектов, делая его жизнь довольно короткой. То же самое касается Entity Framework.