LINQ: работа с данными в декларативном стиле

18 июль, 2008 - 10:25Вячеслав Колдовский

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

Цель практически каждой программы – обработка данных, которые могут быть самыми разнообразными: от простейших типов вроде целых чисел фиксированной разрядности до сложнейших динамических структур, представленных в виде графов в оперативной памяти или взаимосвязанных таблиц в реляционной СУБД. Такое разнообразие обусловливает проблемы в работе с данными – обычно используемые подходы в значительной мере определяются их типом, структурой и источником, а универсальные механизмы фактически отсутствуют.

Сегодня доминируют императивные объектно-ориентированные языки программирования, а ООП вроде бы заметно изменил характер работы с данными. Если ранее они были совершенно «пассивны», а действия над ними осуществляли внешние процедуры и функции, то сейчас они инкапсулируются в объектах, которые достаточно «умны», чтобы самостоятельно выполнять нужные операции. Однако внутри соответствующие методы реализуются, по сути, по старинке – ООП почти никак не повлиял на традиционные методы обработки данных. К тому же применение объектов-контейнеров, содержащих другие объекты, во многих случаях проигрывает по гибкости и эффективности прямому доступу.

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

Кроме того, ООП не слишком популярен в мире БД – здесь правит бал реляционная модель с совершенно особыми правилами и законами. Попытки создания подлинных ОО СУБД предпринимаются с переменным успехом по сей день, однако, видимо, не случайно им пока не удалось потеснить РСУБД – традиционная табличная форма действительно удобна и привычна для представления данных из самых разных сфер нашей деятельности. К тому же стандартизированный язык SQL позволяет формировать запросы в декларативном стиле, что зачастую оказывается куда эффективнее, чем перебор рекурсивных структур посредством циклов. Кстати, декларативный стиль гораздо лучше императивного приспособлен к автоматическому распараллеливанию вычислительных процессов, а такое преимущество сложно переоценить в эпоху многоядерных CPU.

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

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

LINQ

LINQ работа с данными в декларативном стиле
Синтаксическая диаграмма LINQ по своим возможностям практически соответствует SQL

Для того чтобы понять значимость LINQ (Language Integrated Query), следует обратиться к его истории. Релизу, увидевшему свет осенью 2007 г. в составе Visual Studio 2008, предшествовали годы исследований в недрах Microsoft. Первые упоминания о неком основанном на XML специализированном языке для обработки данных X#, разрабатываемом Microsoft Research совместно с Кембриджским университетом, датируются еще 2002 г. Уже тогда планировалось сделать его частью .NET, чтобы упростить разработку веб-сервисов и манипуляцию XML. Затем название X#, во избежание путаницы с C#, было изменено на XEN, а эмблемой выбрана комбинация из круга, треугольника и прямоугольника, которые соответственно обозначают ОО-программирование, иерархические структуры данных, характерные для XML, и реляционные таблицы.

В дальнейшем XEN стал частью экспериментального языка Cω («Си-омега»), также разрабатываемого в Microsoft Research и впервые увидевшего свет в 2004 г. Cω развивался в нескольких направлениях, одним из которых и была унификация работы с данными – многие идеи нашли свое воплощение в LINQ.

Еще один проект, имеющий к этому языку непосредственное отношение, – ObjectSpaces. Эта технология, продемонстрированная на PDC 2001 в составе ADO.NET, представляет собой интерфейс для доступа к информации, позволяющий рассматривать ее как объекты независимо от конкретного хранилища. Технология, кстати, разрабатывалась как часть файловой системы нового поколения WinFS, которая, к сожалению, так и не увидела свет. Зато отдельные идеи из этого проекта перекочевали в LINQ, в частности, на основе ObjectSpaces реализовано взаимодействие с источниками данных SQL-типа.

Таким образом, LINQ – результат многолетних исследований, направленных на то, чтобы дополнить ООП эффективными инструментами для работы с внешними данными, а в перспективе – предложить для этого совершенно новые способы.

На практике

Итак, LINQ представляет собой специализированный язык для работы с данными (впрочем, более корректно все же говорить не просто о языке, а о технологии), и, что очень важно, включен в .NET Framework 3.5 – это, в частности, означает, что его поддержка может быть реализована (на уровне синтаксиса) в любом из .NET-языков, включая и сторонние. В настоящий момент к их числу относятся C# и VB.NET из состава Visual Studio 2008.

Архитектурно LINQ состоит из нескольких уровней: центральный формируют механизмы платформы .NET Framework, верхний обеспечивает поддержка в языках программирования, а нижний – провайдеры доступа к данным. Благодаря этому технология легко расширяется, а отдельные элементы могут развиваться независимо друг от друга.

В исходном коде программы конструкции на LINQ максимально схожи с традиционными SQL-запросами – таким образом разработчики, знакомые с SQL, смогут быстро освоить новую технологию. Разберем простейший пример использования LINQ в C# (для этого достаточно бесплатно распространяемого пакета Visual C# 2008 Express Edition, доступного по адресу microsoft.com/express). Допустим, перед нами стоит тривиальная с точки зрения программирования задача: необходимо сделать выборку данных из некоего массива (скажем, содержащего натуральные числа) по определенному критерию (только четные), а результат вывести в упорядоченном виде.

Традиционный подход предусматривает просмотр массива посредством цикла, выбор элементов, удовлетворяющих критерию, а затем их сортировку (или постепенное добавление в заведомо упорядоченный список). Реализацию в виде консольной программы демонстрирует Листинг 1 (код в комментариях не нуждается, разве что следует обратить внимание, что для сохранения результатов в виде типизированного списка использовались появившиеся в .NET 2.0 обобщения).

В реляционных СУБД (благодаря SQL) подобные задачи решаются куда элегантнее: вместо последовательного перебора данных достаточно описать искомый результат, а декларативный стиль запроса позволяет «уместить» в нем сразу несколько действий. То же самое, по сути, обеспечивает и LINQ. В Листинге 2 цикл для выборки элементов по критерию и последующей сортировки результата заменяет всего одна конструкция, которая по форме и ключевым словам очень сильно напоминает SQL – это и есть LINQ-запрос.

Листинг 1. Выборка и формирование упорядоченного списка элементов массива традиционным способом
static void Main(string[] args)
{
int[] arr = { 10, 5, 13, 18, 4, 24, 65, 41, 30 };
 
List<int> evens = new List<int>();
 
foreach (var number in arr)
{
if (number % 2 == 0)
evens.Add(number);
}
 
evens.Sort();
 
foreach (int number in evens)
Console.WriteLine(number);
}
Листинг 2. Выборка и формирование упорядоченного списка элементов массива с применением LINQ
static void Main(string[] args)
{
int[] arr = { 10, 5, 13, 18, 4, 24, 65, 41, 30 };
 
var evens =
from number in arr
where number % 2 == 0
orderby number
select number;
 
foreach (int number in evens)
Console.WriteLine(number);
 
}
Листинг 3. Группировка слов в массиве по числу символов с применением LINQ
static void Main()
{
string[] words = { «LINQ», «позволяет», «сделать», «многое», 

«гораздо», «проще» };
var groups =
from word in words
orderby word ascending
group word by word.Length into lengthGroups
orderby lengthGroups.Key descending
select new { Length = lengthGroups.Key, Words = lengthGroups };
foreach (var group in groups)
{
Console.WriteLine(«Слова длиной {0} символов», group.Length);
foreach (string word in group.Words)
Console.WriteLine(« « + word);
}
}

Возможности LINQ

Рассмотренный пример иллюстрирует только малую часть возможностей LINQ. Согласно синтаксической диаграмме, практически соответствующей SQL по своим возможностям, в запросе можно производить группировку, объединение, выборку, сортировку, агрегацию и подсчет числа элементов. Соответственно, это позволяет разработчику одним оператором выполнять комплексные операции, скажем, группировку и вывод на экран слов в массиве по количеству символов (Листинг 3) – поверьте, программа в императивном стиле будет выглядеть гораздо более громоздкой.

До настоящего момента мы рассматривали выполнение LINQ-запросов в оперативной памяти, что обеспечивается провайдером LINQ to Objects. Запросы к СУБД и извлечение данных из XML-файлов осуществляются посредством LINQ to SQL и LINQ to XML, которые обладают схожим синтаксисом – отличия заключаются в основном в способах подключения к данным.

LINQ работа с данными в декларативном стиле
LINQPad – свободно доступный удобный инструмент для освоения LINQ

Что касается удобства использования LINQ to SQL, то, наверное, каждый разработчик знает, как трудоемко внедрять SQL в обычный код – необходимо сформировать строку запроса, создать подключение к БД, передать параметры, выполнить запрос, а потом разобрать результат. Самое неприятное, что на протяжении всей процедуры не происходит проверки типов – выявить ошибку, допущенную на этапе кодирования, зачастую оказывается непросто, в таких случаях помогает только тщательное тестирование программы. К тому же даже незначительное изменение структуры БД может привести к непредсказуемым последствиям, поскольку корректность жестко закодированных SQL-запросов не проверяется на этапе компиляции.

LINQ to SQL предназначен для работы совместно с Microsoft SQL Server. Для установления соответствия между реляционными данными и программными классами используется так называемое отображение (mapping), которое обеспечивается дизайнером Visual Studio 2008. Поскольку SQL Server обладает собственным движком исполнения запросов, то LINQ to SQL просто транслирует LINQ-запросы в SQL-код, который затем и обслуживается сервером.

LINQ to Dataset – более универсальный механизм, позволяющий применять LINQ для подключения к любой СУБД посредством компонентов доступа ADO.NET, а также задействовать всю функциональность класса ADO.NET DataSet. А LINQ to Entities ориентирован на использование возможностей технологии ADO.NET Entities Framework, благодаря которой данные представляются как взаимосвязь логической, физической и концептуальной моделей.

В текущей реализации LINQ практикуется отложенное исполнение запросов (deferred execution) – по умолчанию они не исполняются до тех пор, пока в программе не произойдет обращение к их результатам. В отдельных ситуациях подобная оптимизация позволяет повысить скорость работы программы.

Обеспечение поддержки LINQ потребовало дополнения .NET-языков функциональностью, которая может быть интересна и сама по себе. В частности, в C# появились лямбда-выражения, расширяющие и частичные методы, средства построения дерева выражений.

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

Заключение

Безусловно, LINQ – значительный шаг в развитии современных массовых языков программирования. Показательно, что многие идеи, реализованные в LINQ, не были созданы с нуля, а прошли апробацию в других технологиях и языках, в частности, в функциональном программировании и теории реляционных БД. И это можно только приветствовать, ведь для специалиста, знакомого с SQL, освоение LINQ не составит никакого труда, потребуется лишь выяснить детали синтаксиса.

Не случайно сообщество .NET-разработчиков очень положительно восприняло выход LINQ, а расширяемая архитектура позволила инициировать множество сторонних проектов, нацеленных на использование самых различных источников данных – от традиционных СУБД (MySQL, PostgreSQL, Oracle) до поисковых сервисов Google и Windows Search. На текущий момент также известны альтернативные реализации LINQ на основе PHP (PHPLinq) и Java (Quaere).

Впрочем, и сами разработчики из Microsoft не собираются останавливаться на достигнутом, в частности, уже вовсю идут работы над Parallel LINQ (PLINQ), реализацией LINQ, оптимизированной для параллельных вычислений на современных многоядерных процессорах и предполагающей полностью автоматическое распараллеливание выполнения запросов (прежде всего в LINQ to Objects и LINQ to XML).

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

Сайт автора: koldovsky.com