Въведение в 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%’

И като резултат получаваме:

Galcho

George

Достатъчно просто, нали?

Но това е когато изпълняваме заявките на 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

Филтриране с LINQ и C# 3.0

Как става това в 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 възможно.

Проекти с примерите

Проект за VS 2005 + LINQ May 2006 Preview (301KB)

Проект за VS 2008 (34.3KB)

 

Следваща част: Инициализиране на обекти и колекции