Въведение в LINQ и основни възможности
Както споменахме по-горе зад абревиатурата LINQ
стои Language INtegrated
Query. Това е проект на Майкрософт за добавяне синтаксис за заявки,
характерен за T-SQL, в езиците от .NET Framework. Първоначално този синтаксис ще се поддържа от C#
и Visual Basic, но се очаква да бъде въведен и в
останалите езици на по-късен етап.
LINQ дефинира стандартни оператори за заявки, които позволяват
на езиците с поддръжка на
LINQ, да филтрират, изброяват и създават проекции на няколко
типа колекции, като използват единен синтаксис. Основните източници на данни към
момента са масиви, изброими класове ( имплементиращи
интерфейсите ICollection
и IEnumerable ), XML,
DataSet-ове от релационни бази данни. Както в повечето си
продукти Майкрософт са осигурили API (Application Program Interface),
чрез който трети страни могат да осигурят поддръжка на свои източници на данни в
LINQ.
За да стане по-ясно какви са ползите от LINQ
и ще направим паралел със SQL заявките.
Предполагам повечето от вас са запознати със заявките в
SQL, но и да греша това няма да попречи да разберете
извършваните операции.
Филтриране в SQL
Нека имаме таблица Contact в MS
SQL Server със следните колони и данни:
|
Contact
|
|
ID
|
ContactName
|
|
1
|
Galcho
|
|
2
|
George
|
|
3
|
Trifon
|
|
4
|
Kiril
|
|
5
|
Shumi
|
За да изведем всички записи записи от колоната
ContactName, които започват започват с буквата “G”
използваме следната заявка:
|
SELECT [ContactName]
FROM [Contact]
WHERE [ContactName] LIKE ‘G%’
|
И като резултат получаваме:
Достатъчно просто, нали?
Но това е когато изпълняваме заявките на SQL
сървър. А какво се получава, когато вече сме извлекли данните от сървъра и
искаме само да приложим допълнителен филтър!? Тогава вариантите са:
·
Изпълняваме нова заявка към SQL сървъра
заедно с новите условия за филтриране
·
Пишем допълнителен код за филтриране на резултатите.
Уловката тук е, че не винаги се налага да филтрираме данни,
които са взети от SQL сървър. Затова нека разгледаме
филтрирането на масиви и колекции в дотук познатите ни версии на C#.
За всеки пример ще филтрираме този масив:
|
string[] contacts = {
"Galcho",
"George", "Trifon",
"Kiril", "Shumi"
};
|
като ще използваме същото условие – имената да започват с
буквата ‘G’
Филтриране в C# 1.1
За да върнем същият резултат е необходимо да изпълним следният
код:
|
private
void FilterWithCSharp1_1()
{
string[]
contacts = { "Galcho",
"George",
"Trifon",
"Kiril", "Shumi"
};
//display result
foreach
(string
name
in FilterResults(contacts))
{
Console.WriteLine(name);
}
}
private
IEnumerable FilterResults(string[]
Data)
{
//result collection
StringCollection
results = new
StringCollection();
//filter array
foreach
(string
name
in Data)
{
if
(name.StartsWith("G"))
results.Add(name);
}
return
results;
}
|
В този код след
дефиницията на входните данни имаме само една
foreach конструкция, която извежда последователно
всеки елемент върнат от функцията FilterResults,
на която подаваме като параметър масива с входни данни.
В нея декларираме и инициализираме променлива
results от тип
StringCollection.След това с помощта на
foreach конструкция, проверяваме всеки елемент дали
отговаря на зададеното условие ( започва с буква “G” ) и
ако отговаря го добавяме в колекцията с резултатите
results. Накрая връщаме
results като резултат.
Филтриране в C# 2.0
Нека да разгледаме как бихме реализирали тази функционалност със
средствата, които предоставя C# 2.0
|
private
void FilterWithCSharp2()
{
string[]
contacts = { "Galcho",
"George",
"Trifon",
"Kiril", "Shumi"
};
//display result
foreach
(string
name
in FilterResults2(contacts))
{
Console.WriteLine(name);
}
}
private
IEnumerable FilterResults2(string[]
Data)
{
//filter array
foreach
(string
name
in Data)
{
if
(name.StartsWith("G"))
yield return
name;
}
}
|
В този код,
подобно на предишният пример, имаме само една
foreach конструкция, която извежда последователно всеки елемент върнат от
функцията FilterResults2,
на която подаваме като параметър масива с входни данни.
Тънкият момент е във функцията
FilterResults2. Там се осъществява филтрирането на елементите и ако отговарят на
условието се изпълнява оператора yield return, който връща управлението на извикващата функция за всеки
запис по отделно.
Забележка: Препоръчвам да
обходите двата примера в Debug Mode и стъпка по стъпка и
да обрънете внимание, че във функцията
FilterResults първо се обхождат всички елементи, а след
това започва отпечатването на екран. Както посочихме във функцията
FilterResults2 не е така. Това оказва голямо влияние при обработка на
големи колекции, тъй не е необходимо да се изчаква обработката на всички
елементи преди да продължи работата.
Повече за оператора yield
може да прочетете на адрес
http://msdn2.microsoft.com/en-us/library/9k7k7cf0.aspx
Как става това в C# 3.0? Нека разгледаме
следващият код:
|
private static
void FilterWithCSharp3() {
string[]
contacts = { "Galcho",
"George",
"Trifon",
"Kiril", "Shumi"
};
var
result = from
s
in contacts
where
s.StartsWith("G")
select
s;
//display result
foreach
(string
name
in result) {
Console.WriteLine(name);
}
}
|
След първоначално необичайния синтасис изглежда много елегантно,
нали?! Много прилича на SQL заявката.
Същият резултат може да бъде постигнат и по алтернативен начин
като използваме новите методи дефинирани в LINQ
асемблитата.
|
private
static
void
FilterWithCSharp3_2() {
string[]
contacts = { "Galcho",
"George",
"Trifon",
"Kiril", "Shumi"
};
var
result = contacts.Where<string>(x
=> x.StartsWith("G"));
//display result
foreach
(string
name
in result) {
Console.WriteLine(name);
}
}
|
По този начин става дори още по-елегантно.
Този пример е достатъчно кратък, за да покаже измененията в
синтаксиса, обема код и читаемостта. Показахме само използването само на един от
операторите за заявки – where.
Нека да направим нещата малко по-сложни и да групираме данните.
Групиране с LINQ
и C# 3.0
За да групираме данни ще имаме нужда от по-сложни данни. Нека да
дефинираме клас Employee
по следният начин:
|
public
class
Employee
{
public string
Name;
public string
Department;
public double
Salary;
}
|
Обърнете внимание, че не дефинираме конструктор и компилатора
добавя конструктор по подразбиране (без параметри).
Забележка: В реални проекти
е добре да дефинираме свойства, които да достъпват тези полета, но с цел
опростяване на кода сега ги пропускаме.
Дефинираме и инициализираме колекция от обекти от тип
Employee със следният код:
|
Employee[] employees = {
new Employee{Name="Joe", Department="IT",
Salary=1800d},
new Employee{Name="Peter", Department="IT",
Salary=2000d},
new Employee{Name="Jana", Department="Sales",
Salary=900d},
new Employee{Name="Schumacher", Department="Motor
Sport",
Salary=1000000d},
new Employee{Name="Mary", Department="Sales",
Salary=700d},
new Employee{Name="July", Department="Marketing",
Salary=2800d},
};
|
В следващият код ще групираме служителите според техният отдел и
за да направим нещата по-интересни, ще сумираме заплатите на всички служители в
отдела. Вече не е толкова лесно да го направим със средствата на C# 2.0,
нали?!
|
var result =
from e
in
employees
group
e by
e.Department
into g
select new
{
Department = g.Key,
Employees = g,
SumSalaries = g.Sum(e => e.Salary)};
//display result
foreach (var dept
in
result) {
//display department name
Console.WriteLine("Department Name:{0} Sum of
Salaries:{1}",
dept.Department, dept.SumSalaries);
//display employees in current department
foreach
(Employee
empl
in dept.Employees) {
Console.CursorLeft
= 4;
Console.WriteLine("Name:{0}, Salary:{1}",
empl.Name, empl.Salary);
}
}
|
В тази заявка се изпълняват две основни функции:
·
Групиране на служителите по отдели и
·
Сумиране на заплатите на служителите за всеки отдел.
Като резултат се връща колекция от нов тип обекти със свойства:
·
Department - текстов низ с името на отдела
·
Employees - колекция от обекти тип със служителите за
конкретният отдел
·
SumSalaries - число съдържащо сумите на заплатите на
служителите в отдела
Понеже имаме две (вложени) колекции, за да визуализираме
резултата трябва да реализираме два цикъла (също вложени). И ето какъв е изхода
на екран:
|
Department Name:IT Sum of Salaries:3800
Name:Joe, Salary:1800
Name:Peter, Salary:2000
Department Name:Sales Sum of Salaries:1600
Name:Jana, Salary:900
Name:Mary, Salary:700
Department Name:Motor Sport Sum of Salaries:1000000
Name:Schumacher, Salary:1000000
Department Name:Marketing Sum of Salaries:2800
Name:July, Salary:2800
|
Съвет: Преминете през кода
стъпка по стъпка като проверявате състоянието на променливите. По този начин ще
вникнете в детайли какво става на всеки ред от кода.
Много е вероятно да се чувствате объркани от този синтаксис. В
следващите части ще се обясним новите езикови конструкции и ще се върнем към
задълбочено обяснение на LINQ синтаксиса като ще разгледаме още примери.
До момента разгледахме два от операторите за заявки -
where и
group by, А има още
достатъчно много (order, join, union, distinct,
except, intersect), чието имплементиране в .NET
1.1 и .NET 2.0 не е толкова лесно.
Тяхното използване ще покажем по-нататък в това ръководство.
Нека сега да се върнем малко назад и да разгледаме разширенията в синтаксиса на
C#, които правят LINQ възможно.
Следваща
част: Инициализиране на обекти и колекции