Зачем же потребовалось изобретать новый велосипед? Ответ очевиден — нынешним разработчикам необходим современный уровень абстрагирования методов и данных, позволяющий избавиться от банальных академических алгоритмов и довести до ума приемы универсализации, пришедшие из XX в.
Всем желающим до конца разобраться в истории вопроса очень рекомендую заглянуть сюда: http://ru.wikipedia.org/wiki/Лисп. Благодаря поддержке платформы .NET 3.5 среда Delphi Prism дает возможность применять LINQ при написании программ. Акцент языковой парадигмы (в данном случае языки Паскаль, Object Pascal, Delphi, Delphi Prism) становится менее заметен при совместном использовании с самой платформой .NET, а LINQ — лишнее тому подтверждение.
Технология LINQ может делать все то, с чем худо-бедно справлялся SQL, и даже то, что приходилось доделывать программистам, создавая специальные процедуры и бинарные модули. Многие алгоритмы теперь реализуются более изящно с точки зрения предметной области задачи и более размыто с точки зрения классического клиент-серверного дуализма. Хорошо это или плохо, как всегда, определяется квалификацией разработчика.
Рассмотрим возможности использования LINQ в среде Delphi Prism, начав с традиционного «Здравствуй, мир!»:
namespace ConsoleApplication1;
interface uses system.Linq;
type
ConsoleApp = class public
class method Main;
end;
implementation class method ConsoleApp.Main;
begin var Слова:Array of string:=['Здравствуй', 'прекрасный', 'замечательный','мир','LINQ'];
var КороткиеСлова:=from Слово in Слова
where Слово.Length<=5 select Слово;
for each Слово in КороткиеСлова do Console.WriteLine(Слово);
Console.ReadLine;
end;
end.
В данном примере не ведется работа с базами данных или с XML, но хорошо показан общий принцип действия технологии. В основном методе класса описывается массив строковых значений, а затем простыми словами то, что нужно от массива, и сразу же организуется цикл с выводом результатов. Скептики могут возразить, отметив, что в этом подходе нет ничего нового. Однако оцените, на каком замечательном витке повторилась чья-то старая идея. Я имею в виду прежде всего уровень интеграции языков. Чтобы все это стало возможным, платформе .NET пришлось пройти определенный эволюционный путь, и его основные моменты мы и рассмотрим ниже.
Вывод типа (Type Inference)
Компилятор легко узнает тип значения, которое будет результатом какого-либо выражения. В таком случае почему бы ему не догадаться о типе того идентификатора, которому будет присвоено это значение? Как раз для этого и был введен вывод типов (Type Inference). И тогда стало возможным писать вот так:
var i:=1;
var s:="SomeString";
var z:=i + i * i;
var c:='c';
А чтобы убедиться, что вы с компилятором имеете в виду одно и то же, вызовите метод GetType():
Console.WriteLine(i.GetType()); // System.Int32
Console.WriteLine(s.GetType()); // System.String
Console.WriteLine(c.GetType()); // System.Char
Console.WriteLine(z.GetType()); // System.Int32
Но возможности неявно указывать тип оказалось недостаточно, и тогда были придуманы анонимные типы, вообще не имеющие своего имени:
// Вот так выглядит объявлениеь //нового типа:
var anon:=new class(a:= 3, b:= 4.81, c:= 'string data');
Console.WriteLine(anon.a.ToString+' '+anon.b.ToString+' '+anon.c);
А сейчас проведем эксперимент — создадим два одинаковых анонимных типа для разных имен идентификаторов и выведем информацию об этих типах на экран:
var anon:=new class(a:= 3, b:= 4.81, c:= 'string data');
Console.WriteLine(anon.GetType());
var anon2:=new class(a:= 3, b:= 4.81, c:= 'string data');
Console.WriteLine(anon2.GetType());
Таким образом, получилось, что оба раза мы описали один и тот же тип, хотя он и был анонимным.
Теперь повторим эксперимент, но чуть-чуть изменим описание первого анонимного класса:
var anon:=new class(a:= 3, b:= '4.81', c:= 'string data');
Console.WriteLine(anon.GetType());
var anon2:=new class(a:= 3, b:= 4.81, c:= 'string data');
Console.WriteLine(anon2.GetType());
В итоге мы моментально получаем уже два анонимных класса. Расширяющие методы (Extension Methods) Это поистине волшебное средство. Прямо на ходу, без всяких предварительных объявлений мы применяем в каком-либо экземпляре метод, вообще не имеющий к нему никакого отношения:
//Расширяющие методы (Extension Methods)
var parts:= array of String(['Я', 'набрал', 'телефонный', 'номер']);
Console.WriteLine(parts.Implode(','));
Никакого метода Implode у массива не существует! Но пример компилируется, запускается и даже выполняет то, что от него ожидалось. Все оказывается просто — черную работу за нас теперь будет делать трудяга-компилятор. Эта самоотверженная программа начинает искать описание подходящего метода по всем доступным пространствам имен. А если программист сам где-нибудь рядышком опишет подходящий статический класс для подобных целей, то успех будет гарантирован:
type
[Extension]
ArrayExtender = public static class
private
public
[Extension]
class method Implode(parts : array of String; glue : String) : String;
end;
…
class method ArrayExtender.Implode(parts : array of String; glue : String) : String;
begin
var sb := new System.Text.StringBuilder;
for p in parts index i do
begin
sb.Append(p);
if i < parts.Length-1 then
sb.Append(glue);
end;
result := sb.ToString;
end;
На выходе получим строку из бывших членов массива, разделенных запятыми.
Лямбда-выражения.
Здесь лучше сначала показать, а потом рассказать:
//Лямбда-выражения (Lambda Expression)
Личность = public class
public
Имя:String;
Возраст:Integer;
end;
…
var Люди:=new List<Личность>;
Люди.Add(new Личность(Имя := 'Ольга', Возраст := 52));
Люди.Add(new Личность(Имя := 'Катерина', Возраст := 28));
Люди.Add(new Личность(Имя := 'Яков', Возраст := 89));
for each Человек in Люди.Where(p -> p.Возраст < 30)
do Console.Writeline(Человек.Имя);
Думаю, нетрудно догадаться, что до вывода на консоль в этом примере «доживет» только «Катерина». Лучше, чем это сделано у Microsoft, сказать о лямбда-выражениях нельзя : «Лямбда-выражение — это анонимная функция, которая содержит выражения и операторы…» ( http://msdn.microsoft.com/ru-ru/library/bb397687.aspx).
Ленивые вычисления
С концептуальной точки зрения ленивые вычисления вполне можно было бы обозвать «вьюшниками» (VIEW — просмотр в языке SQL). Принцип действия у них одинаковый — разработчик загодя описывает, что и как подготовить, применяя какие-то входные данные, а уже в момент непосредственного запроса с использованием этого описания срабатывает заготовленный алгоритм:
…
//Ленивые вычисления (Lazy Evaluation)
//Сначала описываем, что нам будет
//надо получить от "Группы"
var Группа:=from Человек in Люди
where Человек.Имя.StartsWith('Я');
//Теперь находим и выводим в консоль нужного нам человека
for each Человек in Группа do Console.Writeline('Человек='+Человек.Имя);
…
LINQ и базы данных
Рассмотрев основные возможности LINQ, перейдем к написанию более серьезной программы, например для работы с БД. В качестве учебной базы будет использована база EMPLOYEE.FDB, входящая в поставку с СУБД FireBird 2.0.
И в глобальной Сети, и в «бумажных» изданиях в качестве примера чаще выбирается база данных сервера MS SQL. Чтобы как-то способствовать исправлению сложившейся ситуации, приведу рутинную часть «танцев с бубнами», касающуюся подключения к базе и доступа к данным с помощью ADO.NET:
…
var fbconnection:= 'Database=C:Program FilesFirebirdexamplesempbuildEMPLOYEE.FDB;' + 'User=SYSDBA;' + 'Password=masterke;' + 'Dialect=3;' + 'Server=localhost';
var dbcon:= new FbConnection(fbconnection);
dbcon.Open();
var dbcmd:=dbcon.CreateCommand();
dbcmd.CommandText:= 'SELECT EMP_NO,FIRST_NAME,LAST_NAME,PHONE_EXT,HIRE_DATE, '+ 'DEPT_NO,JOB_CODE,JOB_GRADE,JOB_COUNTRY,SALARY, '+ 'FULL_NAME FROM EMPLOYEE; ';
dbcmd.CommandType:=CommandType.Text;
var adapter:=new FbdataAdapter(dbcmd);
var data:=new DataSet;
adapter.Fill(data,'EMPLOYEE');
var EMPLOYEE:=data.Tables['EMPLOYEE'];
// Просто выводим данные
var EMPLOYEESET:=from EMPLOYEE in EMPLOYEE.AsEnumerable();
for each EMPL in EMPLOYEESET do System.Console.WriteLine(EMPL.Field('FIRST_NAME'));
…
Любой объект, реализующий интерфейс IEnumerable (в нашем случае этот интерфейс обеспечивается методом AsEnumerable), может участвовать в запросе. Приведенный пример — как раз тот случай, когда компилятор уже не имеет никакого понятия о типе данных, который нам надо получить, и потому приходится его явно указывать.
Теперь попробуем оставить в результирующем наборе только фамилии, начинающиеся на латинскую букву B, и кроме того, ограничим результат десятью строками:
…
var EMPLOYEESET2:=from EMPLOYEE in EMPLOYEE.AsEnumerable()
where EMPLOYEE.Field('LAST_NAME').First='B'
order by EMPLOYEE.Field('LAST_NAME')
take 10;
for each EMPL in FROM EMPL2 in EMPLOYEESET2 where (EMPL2.Field('LAST_NAME').length<6) do
System.Console.WriteLine( EMPL.Field('FIRST_NAME')+Chr(9)+ EMPL.Field ('LAST_NAME'));
…
Из рассмотренного примера хорошо видно, что в Delphi Prism может быть использован комбинированный синтаксис для циклов и запросов — конечный набор условий определяется здесь уже на стадии вывода данных в консоль.
А сейчас объединим данные из двух табличек, не прибегая к помощи специальных средств языка SQL:
…
var dbcmd1:=dbcon.CreateCommand(); var dbcmd2:=dbcon.CreateCommand();
dbcmd1.CommandText:='SELECT * FROM employee;';
dbcmd2.CommandText:='SELECT * FROM department;';
var adapter1:=new FbdataAdapter(dbcmd1); var adapter2:=new FbdataAdapter(dbcmd2);
var dataset1:=new DataSet; var dataset2:=new DataSet;
adapter1.Fill(dataset1); adapter2.Fill(dataset2);
var Сотрудники:=dataset1.Tables[0]; var Отделы:=dataset2.Tables[0];
var RESULTS:=from empl in Сотрудники.AsEnumerable
join dep in Отделы.AsEnumerable
on empl.Field('DEPT_NO') equals
dep.Field('DEPT_NO') take 10 select new class(Фамилия:String:=empl.Field ('LAST_NAME'),
Расположение:String:=dep.Field('LOCATION'));
for each КтоГде in RESULTS
do System.Console.WriteLine(КтоГде.Фамилия+'-'+КтоГде.Расположение);
Возможно, текст программы для MS SQL выглядел бы чуть-чуть изящнее, но дело сейчас не в этом. Фактически наш сервер FireBird ничего не знает о том, что и с чем мы хотим объединять, а значит, процесс объединения происходит уже на стороне клиента. Отсюда и вытекают всевозможные минусы и плюсы данной технологии. В этом примере мы немного доработали технологию вывода данных, заранее преобразовав наши результаты неизвестного типа в последовательность объектов аккуратно описанного нами класса.
LINQ и XML
Использовать XML по-прежнему модно, так что приведем пример и на эту тему. Для начала создадим текст XML:
var Книжечки:sequence of КНИГА:= [new КНИГА('Баранкин, будь человеком!', 'Валерий Медведев', 2005),
new КНИГА('Стихи детям', 'Агния Барто', 2007),
new КНИГА('Надежда каждого цветка', 'Трина Паулус', 2006)];
var xml:=new XElement('books', from Книжка in Книжечки where Книжка.Год <> 2006 select new XElement('book', new XAttribute('title', Книжка.Заголовок), new XElement('author', Книжка.Автор) ) );
Console.WriteLine(xml);
xml.Save('C:ooks.xml');
Класс XMLElement принадлежит пространству имен System.XML.Linq (библиотека System.XML.Linq.dll). Для тех, кто не знает: поддержка технологии LINQ реализована в .NET Framework 3.5 в виде набора отдельных библиотек, и ссылки на них надо давать при компиляции. Для конструктора XMLElement мы указываем на входе имя элемента, который надо создать, а также перечень вложенных элементов. В данном случае это будут один элемент и один атрибут. Наверное, те, кто хорошо знаком с пространством имен System.Xml, согласятся, что аналогичные традиционные способы работы с XML без применения System.XML.Linq более утомительные и громоздкие.
Теперь попробуем выборочно прочитать файл books.xml:
var Книги:=from Книга in XElement.Load('C:ooks.xml').XPathSelectElements('//book')
order by Книга.Attribute('title').Value skip 1;
for each Книга in Книги do
Console.WriteLine('Название:'+Книга.Attribute('title').Value+char(9)+ 'Автор: ' +Книга.Element('author').Value);
Первое, что бросается в глаза, метод XPathSelectElements, которому в качестве входного параметра было указано так называемое XPath-выражение. В общем, прочитать XML оказалось так же просто, как и написать его.
LINQ и текстовые файлы
На закуску проверим возможности LINQ, касающиеся обработки текстовых файлов. Создадим текстовый файл, например, путем экспорта данных от имеющегося просмотра PHONE_LIST давно знакомой учебной базы EMPLOYEE.FDB. Экспортируем эту табличку в файл C:PHONE_LIST.csv с помощью любимой программы IBExpert. Ниже приведен пример программы для чтения такого файла:
var Записи:= (from Запись in (from line in File.ReadAllLines('C: PHONE_LIST.csv) select new class( Имя:=line.Split(';')[1], Фамилия:=line.Split(';')[2],
Расположение:=line.Split(';')[4])) order by Запись.Имя).Take(15);
for each Запись in Записи do
System.Console.WriteLine(Запись.Имя+Char(9)+Запись.Фамилия+ Char(9)+Запись.Расположение);
System.Console.ReadLine;
Фактически в одном и том же запросе (формально все-таки в двух — внешнем и вложенном) описаны:
-
правила для чтения файла;
-
разбивка строки файла на поля;
-
преобразование строки в экземпляр класса;
-
сортировка по выбранному столбцу;
-
ограничение на вывод результата.
Как и в SQL, все это сформулировано на понятном, «почти» английском языке. После использования данного запроса мы получаем уже структурированную, типизированную и отсортированную последовательность элементов.
В заключение следует отметить следующее. Для разработчиков на Delphi особенно важно то, что среда программирования Delphi Prism позволяет в настройках проекта регулировать степень совместимости синтаксиса языка с классическим синтаксисом Delphi. Это будет полезно при миграции с обычной Delphi на платформу .NET уже существующих алгоритмов, не слишком привязанных к VCL. Кроме того, «дельфисты» смогут создавать новые проекты в максимально благоприятной для себя атмосфере.
В статье не были досконально рассмотрены особенности реализации схемы запросов языка LINQ на базе Delphi Prism (например, в сравнении с применением LINQ в C#), и потому у читателей есть возможность исследовать их самостоятельно.
Литература
C# 2008 и платформа .NET 3.5 для профессионалов. Киев: Диалектика, 2008.
Pialorsi Paolo, Russo Marco. Introducing Microsoft LINQ. Microsoft Press, 2007.
Kimmel Paul. LINQ Unleshed for C#. SAMS, 2009.
Kumar N. Satheesh. LINQ Quickly. Packit Publishing, 2007.
Albahari Joseph, Albahari Ben. LINQ Pocket Reference. O'Reilly, 2008.
Fabrice Marguerie, Eichert Steve, Wooley Jim. LINQ in Action. Manning, 2008.
Vijay P. Mehta. Pro LINQ Object Relational Mapping with C# 2008. Apress, 2008.
Joseph C., Rattz, Jr. Pro LINQ Language Integrated Query in C# 2008. Apress, 2007.
Ferracchiati Fabio Claudio. LINQ for Visual C# 2008. Apress, 2008.