LINQ. Язык интегрированных запросов в C# 2010 для профессионалов

LINQ. Язык интегрированных запросов в C# 2010 для профессионалов
Автор: Адам Фримен, Джозеф Раттц
Год: 2011
ISBN: 978-5-8459-1701-0
Страниц: 656
Язык: Русский
Формат: PDF
Размер: 10 Мб

Download

LINQ. Язык интегрированных запросов в C# 2010 для профессионалов – Благодаря этой книге, вы изучите следующие вопросы:
Как эффективно задействовать все новые возможности языка C# 2010, связанные с LINQ, включая методы расширений, лямбда-выражения, анонимные типы данных и частичные методы.
Как использовать LINQ to Objects для запроса информации из коллекций данных, расположенных в памяти, таких как массивы, ArrayList и списки.
Почему некоторые запросы являются отложенными, как они влияют на код, и каким образом заставить их работать максимально эффективно.
Как использовать LINQ to XML для создания, манипулирования и поиска в XML-данных.
Как осуществлять запросы в наборы данных с помощью LINQ to DataSet, сосуществуя с унаследованным кодом, и как использовать LINQ для работы с базами данных, отличными от SQL Server.
Как запрашивать базы данных посредством LINQ to SQL, создавать собственные сущностные классы и обрабатывать конфликты параллельного доступа.
Книга рассчитана на программистов разной квалификации, а также будет полезна для студентов и преподавателей дисциплин, связанных с программированием и разработкой для .NET.

+

ГЛАВА 1
Знакомство с LINQ

Листинг 1.1. Программа “Hello LINQ”

using System;
using System.Linq;
string[] greetings = {"hello world", "hello LINQ", "hello Apress"};
var items =
from s in greetings
where s.EndsWith("LINQ")
select s;
foreach (var item in items)
Console.WriteLine(item);

На заметку!
Код из листинга 1.1 был добавлен в проект, созданный с помощью шаблона консольного приложения в Visual Studio 2010. Если это еще не сделано, добавьте директиву usingдля пространства имен System.Linq.

Запуск приведенного выше кода по нажатию <Ctrl+F5>выдаст следующий вывод в окно консоли:
hello LINQ

Сдвиг парадигмы
Вы почувствовали только что, как ваш мир сдвинулся с места? Как разработчик .NET, вы должны были это почувствовать. То, что продемонстрировано в тривиальном примере программы из листинга 1.1, похоже на запрос на языке структурированных запросов (Structured Query Language — SQL) к массиву строк. Взгляните на конструкцию where. Она выглядит так, будто использовался метод EndsWithобъекта string, потому что так оно и есть. Может возникнуть вопрос: а как насчет типа переменной var? Выполняет ли по-прежнему компилятор C# контроль типов? Ответ — да, он проверяет статически типы во время компиляции. Какое средство или средства C# позволяют все это? Ответ: Microsoft Language Integrated Query (язык интегрированных запросов Microsoft), иначе называемый LINQ.

Запрос к XML
В то время как пример из листинга 1.1 достаточно тривиален, пример в листинге 1.2 начинает отражать потенциальную мощь, которую вручает LINQ в руки разработчика .NET. Он демонстрирует легкость, с которой можно взаимодействовать и опрашивать данные XML (Extensible Markup Language — расширяемый язык разметки) с помощью API-интерфейса LINQ to XML. Обратите внимание, как на основе данных XML конструируется объект по имени books, с которым впоследствии можно взаимодействовать программно.
Листинг 1.2. Простой запрос к XML-разметке с использованием LINQ to XML

using System;
using System.Linq;
using System.Xml.Linq;
XElement books = XElement.Parse(
@"<books>
<book>
<title>Pro LINQ: Language Integrated Query in C# 2010</title>
<author>Joe Rattz</author>
</book>
<book>
<title>Pro .NET 4.0 Parallel Programming in C#</title>
<author>Adam Freeman</author>
</book>
<book>
<title>Pro VB 2010 and the .NET 4.0 Platform</title>
<author>Andrew Troelsen</author>
</book>
</books>");
var titles =
from book in books.Elements("book")
where (string) book.Element("author") == "Joe Rattz"
select book.Element("title");
foreach(var title in titles)
Console.WriteLine(title.Value);

На заметку!
Код в листинге 1.2 требует добавления к ссылкам проекта сборки System.Xml. Linq.dll, если это еще не сделано. Также обратите внимание, что добавлена директива usingдля пространства имен System.Xml.Linq.

Запуск предыдущего кода нажатием <Ctrl+F5>приводит к выводу следующих данных в окно консоли:
Pro LINQ: Language Integrated Query in C# 2010
Обратите внимание на то, как данные XML были разобраны для помещения в объект XElement. Объект XmlDocumentнигде не создавался. Среди преимуществ LINQ to XML — расширения, которые он привносит в XML API. Теперь вместо того, чтобы сосредоточивать все вокруг XmlDocument, как того требует W3C Document Object Model (DOM) XML API, интерфейс LINQ to XML позволяет разработчику взаимодействовать на уровне элемента, используя класс XElement.

На заметку!
В дополнение к средствам запросов, LINQ to XML предоставляет более мощный и простой способ использования интерфейса для работы с данными XML.

Здесь также применялся SQL-подобный синтаксис для опроса данных XML, как если бы это была база данных.

Запрос к базе данных SQL Server
В следующем примере демонстрирует использование LINQ to SQL для опроса таблиц базы данных. В коде из листинга 1.3 выполняется запрос к стандартной базе данных примеров Microsoft Northwind.
Листинг 1.3. Простой запрос XML с использованием LINQ to XML

using System.Linq;
using System.Data.Linq;
using nwind;
Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");
var custs =
from c in db.Customers
where c.City == "Rio de Janeiro"
select c;
foreach (var cust in custs)
Console.WriteLine("{0}", cust.CompanyName);

На заметку!
Код в листинге 1.3 требует добавления сборки System.Data.Linq.dllк списку ссылок проекта, если это не сделано ранее. Также обратите внимание, что добавлена директива usingдля пространства имен System.Xml.Linq.
В коде была добавлена директива usingдля пространства имен nwind. В этом примере должна использоваться утилита командной строки SQLMetal или Object Relational Designer, чтобы сгенерировать сущностные классы для целевой базы данных, которой в данном случае является база данных примеров Microsoft Northwind. В главе 12 показано, как это делать с SQLMetal. Сгенерированные сущностные классы создаются в пространстве имен nwind, которое указано при их генерации. После этого в проект добавляется сгенерированный SQLMetal исходный модуль, а также директива usingдля пространства имен nwind.
На заметку!
Для корректной установки соединения может понадобиться изменить строку соединения, переданную конструктору Northwind в листинге 1.3. В разделе “DataContext()и [Your] DataContext()” главы 16 описаны разные способы подключения к базе данных.
Запуск предыдущего кода нажатием <Ctrl+F5>приводит к выводу следующих данных в окно консоли:
Hanari Carnes
Que Delícia
Ricardo Adocicados
В этом примере демонстрируется опрос таблицы Customersиз базы данных Northwind на предмет списка заказчиков из Рио-де-Жанейро (Rio de Janeiro). Хотя может показаться, что здесь не происходит ничего нового или особенного, чего нельзя было бы получить существующими средствами, все же есть серьезные отличия. Важнее всего то, что этот запрос интегрирован в язык, а это значит, что получается поддержка уровня языка, включающая проверку синтаксиса и средство IntelliSense. Ушли в прошлое те дни, когда запрос SQL записывался в строку, и ошибку невозможно было обнаружить вплоть до выполнения кода. Вы хотите сделать конструкцию whereзависящей от поля таблицы Customers, но не помните имени этого поля? Средство IntelliSense покажет поля таблицы. Как только вы введете c.в предыдущем примере, IntelliSense отобразит все поля таблицы Customers.
Во всех предыдущих запросах используется синтаксис выражений запросов. В главе 12 вы узнаете, что существуют два синтаксиса запросов LINQ, один из которых — синтаксис выражений запросов. Конечно, можно также применять привычный для C# синтаксис стандартной точечной нотации. Этот синтаксис предлагает нормальный шаблон вызовов объект.метод().

Появление LINQ
По мере становления платформы .NET Framework и поддерживаемых ею языков C# и VB, стало ясно, что одной из наиболее проблемных областей для разработчиков остается доступ к данным из разных источников. В частности, доступ к базе данных и манипуляции XML часто в лучшем случае запутаны, а в худшем — проблематичны.
Проблемы, связанные с базами данных, многочисленны. Первая сложность в том, что нельзя программно взаимодействовать с базой данных на уровне естественного языка. Это приводит к синтаксическим ошибкам, которые не проявляются вплоть до момента запуска. Неправильные ссылки на поля базы данных тоже не обнаруживаются. Это может пагубно отразиться на программе, особенно если произойдет во время выполнения кода обработки ошибок. Нет ничего хуже, чем сбой механизма обработки ошибок из-за синтаксически неверного кода, который никогда не тестировался. Иногда это неизбежно из-за непредсказуемого поведения ошибки. Наличие кода базы данных, который не проверяется во время компиляции, определенно может привести к этой проблеме.
Вторая проблема связана с неудобством, которое вызвано различными типами данных, используемыми определенным доменом данных, например, разница между типами базы данных или типами XML и типами данных в языке, на котором написана программа. В частности, серьезные сложности могут вызывать типы времени и даты. Разбор, итерация и манипулирование XML-разметкой могут быть достаточно утомительными. Часто фрагмент XML — это все, что нужно, но из-за требований интерфейса W3C DOM XML API объект XmlDocumentдолжен быть обязательно создан, чтобы выполнять различные операции над фрагментом XML.
Вместо того чтобы просто добавить больше классов и методов для постепенного восполнения этих недостатков, в Microsoft решили пойти на один шаг дальше в абстрагировании основ запросов данных из этих конкретных доменов данных. В результате появился LINQ — технология Microsoft, предназначенная для поддержки запросов к данным всех типов на уровне языка. Эти типы включают массивы и коллекции в памяти, базы данных, документы XML и многое другое.

LINQ поддерживает запросы данных
По большей части LINQ ориентирован на запросы — будь то запросы, возвращающие набор подходящих объектов, единственный объект или подмножество полей из объекта либо набора объектов. В LINQ этот возвращенный набор объектов называется последовательностью(sequence). Большинство последовательностей LINQ имеют тип IEnumerable<T>, где T— тип данных объектов, находящихся в последовательности. Например, если есть последовательность целых чисел, они должны храниться в переменной типа IEnumerable<int>. Вы увидите, что IEnumerable<T>буквально господствует в LINQ. Очень многие методы LINQ возвращают IEnumerable<T>.

LINQ to Objects
LINQ to Objects — название, данное API-интерфейсу IEnumerable<T>для стандартных операций запросов (Standard Query Operators). Именно LINQ to Objects позволяет выполнять запросы к массивам и находящимся в памяти коллекциям данных. Стандартные операции запросов — это статические методы класса System.Linq. Enumerable, которые используются для создания запросов LINQ to Objects.
LINQ to XML
LINQ to XML — название, назначенное API-интерфейсу LINQ, который ориентирован на работу с XML. В Microsoft не только добавили необходимые библиотеки XML для работы с LINQ, но также восполнили недостатки стандартной модели XML DOM, существенно облегчив работу с XML. Прошли времена, когда нужно было создавать XmlDocumentтолько для того, чтобы поработать с небольшим фрагментом XML-кода. Чтобы воспользоваться преимуществами LINQ to XML, в проект понадобится добавить ссылку на сборку System.Xml.Linq.dllи директиву usingследующего вида:
using System.Xml.Linq;
LINQ to DataSet
LINQ to DataSet — название, данное API-интерфейсу LINQ, который предназначен для работы с DataSet. У многих разработчиков есть масса кода, полагающегося на DataSet. Те, кто не хотят отставать от новых веяний, но и не готовы переписывать свой код, благодаря этому интерфейсу могут воспользоваться всей мощью LINQ.
LINQ to SQL
LINQ to SQL — наименование, присвоенное API-интерфейсу IQueryable<T>, который позволяет запросам LINQ работать с базой данных Microsoft SQL Server. Чтобы воспользоваться преимуществами LINQ to SQL, в проект понадобится добавить ссылку на сборку System.Data.Linq.dll, а также следующую директиву using:
using System.Data.Linq;
LINQ to Entities
LINQ to Entities — альтернативный API-интерфейс LINQ, используемый для обращения к базе данных. Он отделяет сущностную объектную модель от физической базы данных, вводя логическое отображение между ними двумя. С таким отделением возрастает мощь и гибкость, но также растет и сложность. Поскольку LINQ to Entities не входит в состав ядра LINQ, в настоящей книге он не рассматривается. Если нужна более высокая гибкость, чем обеспечивается LINQ to SQL, имеет смысл рассмотреть эту альтернативу. В частности, когда необходимо ослабить связь между сущностной объектной моделью и базой данных, если сущностные объекты конструируются из нескольких таблиц или требуется большая гибкость в моделировании сущностных объектов, то в этом случае LINQ to Entities может стать оптимальным выбором.
Как получить LINQ
Формально отдельного продукта LINQ, который нужно было бы получать отдельно, не существует. LINQ полностью интегрирован в .NET Framework, начиная с версии 3.5 и Visual Studio 2008. В .NET 4.0 и Visual Studio 2010 добавлена поддержка средств Parallel LINQ, которые рассматриваются в главах 22–24.

LINQ предназначен не только для запросов
Может показаться, что LINQ — это нечто, связанное только с запросами, поскольку расшифровывается как язык интегрированных запросов(Language Integrated Query). Однако не думайте о нем лишь в этом контексте. Предпочтительнее воспринимать LINQ как механизм итерации данных (data iteration engine), но возможно в Microsoft не захотели обозначать эту технологию аббревиатурой DIE (“умереть”).
Приходилось ли вам когда-нибудь вызывать метод, возвращающий данные в структуре, которую затем приходилось преобразовывать в еще одну структуру данных, прежде чем передать другому методу? Предположим, например, что вызывается метод A, и этот метод возвращает массив типа string, содержащий числовые значения в виде строк. Затем нужно вызвать метод B, но метод B требует массива целых чисел. Обычно приходится организовывать цикл для прохода по массиву строк и наполнения вновь сконструированного массива целых чисел. Давайте рассмотрим краткий пример мощи Microsoft LINQ.
Предположим, что имеется массив строк, которые приняты от метода A, как показано в листинге 1.4.

Листинг 1.4. Преобразование массива строк в массив целых

string[] numbers = { "0042", "010", "9", "27" };

В этом примере объявлен статический массив строк. Теперь перед вызовом метода
B нужно преобразовать массив строк в массив целых чисел:
int[] nums = numbers.Select(s => Int32.Parse(s)).ToArray();
Вот и все. Что может быть проще? А вот код, необходимый для отображения результирующего массива целых чисел:
foreach(int num in nums)
 Console.WriteLine(num);
Вывод выглядит следующим образом:
42
10
9
27
Возможно, вы подумали, что просто в строках отброшены ведущие пробелы. Но убедит ли вас, если отсортировать результат? Если бы это были по-прежнему строки, то 9 окажется в конце, а 10 — в начале. В листинге 1.5 приведен код, который выполняет преобразование и сортирует вывод.
Листинг 1.5. Преобразование массива строк в массив целых чисел с последующей сортировкой

string[] numbers = { "0042", "010", "9", "27" };
int[] nums = numbers.Select(s => Int32.Parse(s)).OrderBy(s => s).ToArray();
foreach(int num in nums)
Console.WriteLine(num);

И вот результат:
9
10
27
42

Не правда ли гладко? Вы можете возразить, что все это прекрасно, но пример был очень прост. Давайте рассмотрим более сложный пример.
Предположим, что есть некоторый код, содержащий класс Employee. В этом классе Employeeимеется метод, возвращающий всех сотрудников. Также предположим, что есть другой код, включающий класс Contact, с определенным в нем методом, публикующим контакты. Пусть необходимо опубликовать всех сотрудников в виде контактов. Задача выглядит достаточно простой, но здесь таится ловушка. Общий метод Employee, который извлекает всех сотрудников, возвращает их в списке ArrayList, хранящем объекты Employee, а метод Contact, публикующий контакты, требует массива объектов типа Contact. Ниже показан обычный для такого случая код:

namespace LINQDev.HR
{
public class Employee
{
public int id;
public string firstName;
public string lastName;
public static ArrayList GetEmployees()
{
// Конечно, реальный код здесь должен был бы выполнять запрос к базе данных.
ArrayList al = new ArrayList();
/ / Средство инициализации объектов С# превращает это в пару пустяков.
al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz"} );
al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates"} );
al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg"} );
return(al);
}
}
}
namespace LINQDev.Common
{
public class Contact
{
public int Id;
public string Name;
public static void PublishContacts(Contact[] contacts)
{
/ / Этот метод публикации просто выводит их в окно консоли.
foreach(Contact c in contacts)
Console.WriteLine("Contact Id: {0} Contact: {1}", c.Id, c.Name);
}
}
}

Как видите, класс Employeeи метод GetEmployeesнаходятся в одном пространстве имен — LINQDev.HR, а метод GetEmployeesвозвращает ArrayList. Метод
PublishContactsрасположен в другом пространстве имен — LINQDev.Common, и требует передачи ему массива объектов Contact.
Ранее это всегда требовало итерации по списку ArrayList, который возвращен методом GetEmployees, и создания нового массива типа Contactдля передачи методу
PublishContacts. Как можно видеть в листинге 1.6, LINQ значительно облегчает решение этой задачи.

Листинг 1.6. Вызов обычного кода

ArrayList alEmployees = LINQDev.HR.Employee.GetEmployees();
LINQDev.Common.Contact[] contacts = alEmployees
.Cast<LINQDev.HR.Employee>()
.Select(e => new LINQDev.Common.Contact {
Id = e.id,
Name = string.Format("{0} {1}", e.firstName, e.lastName)
} )
.ToArray<LINQDev.Common.Contact>();
LINQDev.Common.Contact.PublishContacts(contacts);

Чтобы преобразовать коллекцию ArrayListобъектов Employeeв массив объектов Contact, сначала выполняется приведение ArrayListобъектов Employeeк последовательности IEnumerable<Employee>с использованием стандартной операции запросов Cast. Это необходимо, потому что использован унаследованный класс коллекции ArrayList. С синтаксической точки зрения в коллекции ArrayListхранятся объекты класса System.Object, а не объекты типа класса Employee. Поэтому они должны быть приведены к объектам Employee. Если бы метод GetEmployeesвозвращал обобщенную коллекцию List, необходимости в этом не было бы. Однако на момент написания унаследованного кода этот тип коллекции не был доступным.
Затем на возвращенной последовательности объектов Employeeвызывается операция Select, и в лямбда-выражении— коде, переданном внутрь вызова метода Select, —
создается и инициализируется экземпляр объекта Contactс использованием для этого средства инициализации объектов C#, чтобы присвоить значения входного элемента Employeeвновь сконструированному выходному элементу Contact. Лямбда-выражение (lambda expression) — это средство C#, которое является сокращенным способом указания анонимных методов и описано в главе 2. И, наконец, последовательность вновь сконструированных объектов Contact преобразуется в массив объектов Contactс применением операции ToArray, потому что этого требует метод PublishContacts. Разве не изящно? А вот результат:
Contact Id: 1 Contact: Joe Rattz
Contact Id: 2 Contact: William Gates
Contact Id: 3 Contact: Anders Hejlsberg
Как видите, LINQ может делать многое помимо запросов данных. По мере чтения глав книги задумайтесь о дополнительных применениях средств, предоставленных LINQ.

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

Используйте ключевое слово var, когда запутались
Ключевое слово varнеобходимо использовать при захвате последовательности от анонимных классов в переменную, иногда это удобный способ заставить код компилироваться, когда возникает путаница со сложными обобщенными типами. Хотя предпочтительнее подход к разработке, при котором точно известно, какого типа данные содержатся в последовательности — в том смысле, что для IEnumerable<T>должен быть известен тип T— иногда, особенно в начале работы с LINQ, это может вводить в заблуждение. Если обнаруживается, что код не компилируется из-за несоответствия типов данных, попробуйте заменить явно установленные типы переменных на указанные с применением ключевым словом var.
Например, предположим, что есть следующий код:

// Этот код не компилируется.
Northwind db = 
new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");
IEnumerable<?> orders = db.Customers
.Where(c => c.Country == "USA" && c.Region == "WA")
.SelectMany(c => c.Orders);

Может быть неясно, каков тип данных у последовательности IEnumerable. Вы знаете, что это IEnumerableнекоторого типа T, но что собой представляет T? Удобный трюк состоит в присваивании результата запроса переменной, тип которой указан с помощью ключевого слова var, и затем получить тип текущего значения переменной, так что тип Tизвестен. В листинге 1.7 показано, как должен выглядеть такой код.
Листинг 1.7. Пример кода, использующего ключевое слово var

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");
var orders= db.Customers
.Where(c => c.Country == "USA" && c.Region == "WA")
.SelectMany(c => c.Orders);
Console.WriteLine(orders.GetType());

Обратите в этом примере внимание на то, что теперь тип переменной ordersуказан с использованием ключевого слова var. Запуск этого кода даст следующий вывод:
System.Data.Linq.DataQuery`1[nwind.Order]
Здесь присутствует загадочный жаргон компилятора, но интерес представляет часть nwind.Order. Теперь вы знаете, что типом данных полученной последовательности является nwind.Order. Если такой способ не устраивает, запустите пример в отладчике и просмотрите переменную orders в окне Locals(Локальные); для нее будет показан следующий тип данных:
System.Linq.IQueryable<nwind.Order> {System.Data.Linq.DataQuery<nwind.Order>}
Это проясняет тот факт, что вы имеете дело с последовательностью объектов nwind.Order. Формально здесь получается IQueryable<nwind.Order>, но это при желании можно присвоить IEnumerable<nwind.Order>, поскольку IQueryable<T> наследуется от IEnumerable<T>.
Таким образом, можно переписать предыдущий код и добавить перечисление результатов, как показано в листинге 1.8.
Листинг 1.8. Пример кода из листинга 1.7, но с явными типами

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");
IEnumerable<Order> orders = db.Customers
.Where(c => c.Country == "USA" && c.Region == "WA").SelectMany(c => c.Orders);
foreach(Order item in orders)
Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);

На заметку!
Чтобы приведенный код работал, должна быть предусмотрена директива using для пространства имен System.Collectiobs.Genericв дополнение к пространству имен System.Linq, которое всегда используется при работе с кодом LINQ.

Этот код должен дать следующий результат (приведенный с сокращениями):
3/21/1997 12:00:00 AM – 10482 – Lazy K Kountry Store
5/22/1997 12:00:00 AM – 10545 – Lazy K Kountry Store

4/17/1998 12:00:00 AM – 11032 – White Clover Markets
5/1/1998 12:00:00 AM – 11066 – White Clover Markets

Используйте операции Castили OfType для унаследованных коллекций
Вы обнаружите, что большинство стандартных операций запросов LINQ могут быть вызваны на коллекциях, реализующих интерфейс IEnumerable<T>. Ни одна из унаследованных коллекций C# из пространства имен System.Collectionsне реализует IEnumerable<T>. Поэтому возникает вопрос: как использовать LINQ с унаследованными коллекциями?
Есть две стандартных операции запросов, специально предназначенные для этой цели — Castи OfType. Обе они могут использоваться для преобразования унаследованных коллекций в последовательности IEnumerable<T>. В листинге 1.9 показан пример.
Листинг 1.9. Преобразование унаследованной коллекции в IEnumerable<T> с применением операции Cast

// Построим унаследованную коллекцию.
ArrayList arrayList = new ArrayList();
arrayList.Add("Adams");
arrayList.Add("Arthur");
arrayList.Add("Buchanan");
IEnumerable<string> names = arrayList.Cast<string>().Where(n => n.Length < 7);
foreach(string name in names)
Console.WriteLine(name);

В листинге 1.10 представлен пример использования операции OfType.
Листинг 1.10. Использование операции OfType

// Построим унаследованную коллекцию.
ArrayList arrayList = new ArrayList();
arrayList.Add("Adams");
arrayList.Add("Arthur");
arrayList.Add("Buchanan");
IEnumerable<string> names = arrayList.OfType<string>().Where(n => n.Length < 7);
foreach(string name in names)
Console.WriteLine(name);

Оба примера дают одинаковый результат:
Adams
Arthur
Разница между двумя операциями состоит в том, что Castпытается привести все элементы в коллекции к указанному типу, помещая их в выходную последовательность. Если в коллекции есть объект типа, который не может быть приведен к указанному, генерируется исключение. Операция OfTypeпытается поместить в выходную последовательность только те элементы, которые могут быть приведены к указанному типу.

Отдавайте предпочтение операции OfTypeперед Cast
Одной из наиболее важных причин добавления обобщений в C# была необходимость предоставить языку возможность создавать коллекции со статическим контролем типов. До появления обобщений приходилось создавать собственные специфические типы коллекций для каждого типа данных, которые нужно было в них хранить — отсутствовал способ гарантировать, что каждый элемент, помещаемый в унаследованную коллекцию, имеет один и тот же корректный тип. Ничто не могло помешать коду добавить объект TextBoxв ArrayList, предназначенный для хранения только объектов Label.
С появлением обобщений в версии C# 2.0 разработчики получили в свои руки способ явно устанавливать, что коллекция может содержать только элементы заданного типа. Хотя операции OfTypeи Castмогут работать с унаследованными коллекциями, Cast требует, чтобы каждый объект в коллекции относился к правильному типу, что было фундаментальным недостатком унаследованных коллекций, из-за которого появились обобщения. Когда используется операция Castи любой из объектов в коллекции не может быть приведен к указанному типу данных, генерируется исключение. С другой стороны, с помощью операции OfTypeв выходной последовательности IEnumerable<T> будут сохранены только объекты указанного типа, и никаких исключений генерироваться не будет. При лучшем сценарии все объекты относятся к правильному типу, поэтому все попадают в выходную последовательность. В худшем сценарии некоторые элементы будут пропущены, но в случае применения операции Castони привели бы к исключению.

Не рассчитывайте на безошибочность запросов
В главе 3 речь пойдет о том, что запросы LINQ часто являются отложенными и не выполняются сразу при вызове. Например, рассмотрим следующий фрагмент кода из листинга 1.1:
var items =
from s in greetings
where s.EndsWith(“LINQ”)
select s;
foreach (var item in items)
Console.WriteLine(item);
Хотя может показаться, что запрос выполняется при инициализации переменной items, на самом деле это не так. Поскольку операции Whereи Selectявляются отложенными, запрос на самом деле не выполняется в этой точке. Запрос просто вызывается, объявляется или определяется, но не выполняется. Все начинает происходить тогда, когда из него извлекается первый результат. Это обычно происходит при перечислении переменной с результатами запроса. В этом примере результат запроса не востребован до тех пор, пока не запустится оператор foreach. Такое поведение запроса позволяет называть его отложенным.
Очень легко забыть о том, что многие операции запросов являются отложенными и не выполняются до тех пор, пока не начнется перечисление результатов. Это значит, что можно иметь неправильно написанный запрос, который сгенерирует исключение только тогда, когда начнется перечисление его результатов. Такое перечисление может начаться намного позже, так что можно легко забыть, что причиной неприятностей стал неправильный запрос.
Рассмотрим код в листинге 1.11.
Листинг 1.11. Запрос с преднамеренным исключением, отложенным до перечисления

string[] strings = { "one", "two", null, "three" };
Console.WriteLine("Before Where() is called.");
IEnumerable<string> ieStrings = strings.Where(s => s.Length == 3);
Console.WriteLine("After Where() is called.");
foreach(string s in ieStrings)
{
Console.WriteLine("Processing " + s);
}

Известно, что третий элемент в массиве строк — null, и нельзя вызвать null.Length без генерации исключения. Выполнение кода благополучно пройдет строку, где вызывается запрос. Все будет хорошо до тех пор, пока не начнется перечисление последовательности ieStrings, и не дойдет до третьего элемента, где возникнет исключение. Ниже показан результат выполнения этого кода:
Before Where() is called.
After Where() is called.
Processing one
Processing two
Unhandled Exception: System.NullReferenceException: Object reference not set to
an instance of an object.
Необработанное исключение: System.NullReferenceException: Объектная ссылка не
установлена в экземпляр объекта.

Как видите, вызов операции Whereпрошел без исключения. Оно не появилось до тех пор, пока при перечислении не была произведена попытка обратиться к третьему элементу последовательности. Теперь представьте, что последовательность ieStrings передана функции, которая дальше выполняет перечисление последовательности — возможно, чтобы наполнить раскрывающийся список или какой-то другой элемент управления. Легко подумать, что исключение вызвано сбоем в этой функции, а не самим запросом LINQ.

Используйте преимущество отложенных запросов
В главе 3 тема отложенных запросов будет раскрыта более глубоко. Однако здесь следует отметить, что отложенный запрос, который в конечном итоге возвращает
IEnumerable<T>, может перечисляться снова и снова, получая последние данные из источника. В этом случае не нужно ни вызывать, ни, как отмечалось ранее, объявлять запрос заново.
В большинстве примеров кода этой книги вы увидите вызов запроса и возврат IEnumerable<T>для некоторого типа T, сохраняемый в переменной. Затем обычно запускается оператор foreachна последовательности IEnumerable<T>. Это реализовано для демонстрационных целей. Если код выполняется много раз, повторный вызов запроса — лишняя работа. Более оправданным может быть наличие метода инициализации запроса, который вызывается однажды в жизненном цикле контекста, и в котором конструируются все запросы. Затем можно выполнить перечисление конкретной последовательности, чтобы получить последнюю версию результатов из запроса.

Используйте свойство Logиз DataContext
При работе с LINQ to SQL не забывайте, что класс базы данных, генерируемый SQLMetal, унаследован от System.Data.Linq.DataContext. Это значит, что сгенерированный класс DataContextимеет некоторую полезную встроенную функциональность, такую как свойство Logтипа TextWriter.
Одна из полезных возможностей объекта Logсостоит в том, что он выводит эквивалентный SQL-оператор запроса IQueryable<T>до подстановки параметров. Случалось ли вам сталкиваться с отказом кода в рабочей среде, который, как вам кажется, вызван данными? Не правда ли, было бы хорошо запустить запрос на базе данных, вводя его в SQL Enterprise Manager или Query Analyzer, чтобы увидеть в точности, какие данные он возвращает? Свойство Logкласса DataContextвыводит запрос SQL. Соответствующий пример показан в листинге 1.12.
Листинг 1.12. Пример использования объекта DataContext.Log

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");
db.Log = Console.Out;
IQueryable<Order> orders = from c in db.Customers
from o in c.Orders
where c.Country == "USA" && c.Region == "WA"
select o;
foreach(Order item in orders)
Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);

Этот код производит следующий вывод:
SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate],
[t1].[RequiredDate], [t1].[ShippedDate], [t1].[ShipVia], [t1].[Freight],
[t1].[ShipName], [t1].[ShipAddress], [t1].[ShipCity], [t1].[ShipRegion],
[t1].[ShipPostalCode], [t1].[ShipCountry]
FROM [dbo].[Customers] AS [t0], [dbo].[Orders] AS [t1]
WHERE ([t0].[Country] = @p0) AND ([t0].[Region] = @p1) AND ([t1].[CustomerID] =
[t0].[CustomerID])
— @p0: Input String (Size = 3; Prec = 0; Scale = 0) [USA]
— @p1: Input String (Size = 2; Prec = 0; Scale = 0) [WA]
— Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1
3/21/1997 12:00:00 AM – 10482 – Lazy K Kountry Store
5/22/1997 12:00:00 AM – 10545 – Lazy K Kountry Store
6/19/1997 12:00:00 AM – 10574 – Trail’s Head Gourmet Provisioners
6/23/1997 12:00:00 AM – 10577 – Trail’s Head Gourmet Provisioners
1/8/1998 12:00:00 AM – 10822 – Trail’s Head Gourmet Provisioners
7/31/1996 12:00:00 AM – 10269 – White Clover Markets
11/1/1996 12:00:00 AM – 10344 – White Clover Markets
3/10/1997 12:00:00 AM – 10469 – White Clover Markets
3/24/1997 12:00:00 AM – 10483 – White Clover Markets
4/11/1997 12:00:00 AM – 10504 – White Clover Markets
7/11/1997 12:00:00 AM – 10596 – White Clover Markets
10/6/1997 12:00:00 AM – 10693 – White Clover Markets
10/8/1997 12:00:00 AM – 10696 – White Clover Markets
10/30/1997 12:00:00 AM – 10723 – White Clover Markets
11/13/1997 12:00:00 AM – 10740 – White Clover Markets
1/30/1998 12:00:00 AM – 10861 – White Clover Markets
2/24/1998 12:00:00 AM – 10904 – White Clover Markets
4/17/1998 12:00:00 AM – 11032 – White Clover Markets
5/1/1998 12:00:00 AM – 11066 – White Clover Markets

Используйте форум LINQ
Несмотря на приведенные полезные советы, все же более чем вероятно то, что вы периодически будете оказываться в тупике. Не забывайте, что на MSDN.com существует отдельный форум, посвященный LINQ (http: //www.linqdev.com). Этот форум отслеживается разработчиками Microsoft, и на нем можно почерпнуть массу полезных сведений.

Резюме
Вероятно, что вам не терпится перейти к следующей главе, но сначала давайте вспомним несколько важных моментов.
Самое главное — LINQ изменяет способ, которым разработчики .NET выполняют запросы данных. Следует иметь в виду, что LINQ — это не просто новая библиотека для добавления к проекту. Это общий подход к опросу данных, объединяющий несколько компонентов в зависимости от типа опрашиваемого хранилища данных. В настоящее время LINQ можно применять для запросов к следующим источникам данных: коллекциям, находящимся в памяти, используя LINQ to Objects; к XML, используя LINQ to XML; DataSet — с помощью LINQ to DataSet; к базам данных SQL Server с применением LINQ to SQL.
Также помните, что LINQ предназначен не только для запросов. Мы сочли LINQ очень полезным также для форматирования, проверки достоверности и даже извлечения данных с последующим их преобразованием в формат, подходящий для использования в элементах управления Windows Forms и WPF.
И последнее (по порядку, но не по важности): мы надеемся, что вы не будете пренебрегать советами, приведенными в этой главе. Это не проблема, если некоторые из них пока еще не понятны. Все прояснится в процессе чтения книги. Просто вспоминайте о них, когда попадаете в сложную ситуацию.
В следующей главе будут описаны расширения, добавленные Microsoft в язык C#, которые сделали возможным синтаксис и работу LINQ.