Глава 22. Ламбда изрази и LINQ заявки

В тази тема...

В настоящата тема ще се запознаем с част от по-сложните възможности на езика C# и по-специално ще разгледаме как се правят заявки към колекции чрез ламбда изрази и LINQ заявки. Ще обясним как да добавяме функционалност към съществуващи вече класове, използвайки разширя­ващи методи (extension methods). Ще се запознаем с анонимните типове (anonymous types), ще опишем накратко какво представляват и как се използват. Ще разгледаме ламбда изразите (lambda expressions), ще покажем с примери как работят повечето вградени ламбда функции. След това ще обърнем по-голямо внимание на синтаксиса на LINQ. Ще научим какво представлява, как работи и какви заявки можем да конструираме с него. Накрая ще се запознаем с ключовите думи за LINQ, тяхното значение и ще ги демонстрираме, чрез голям брой примери.

Съдържание

Видео

Презентация

Мисловни карти


Разширяващи методи (extension methods)

Често пъти в практиката на програмистите им се налага да добавят функционалност към вече съществуващ код. Ако кодът е наш, можем просто да добавим нужната функционалност и да прекомпилираме. Когато дадено асембли (.exe или .dll файл) е вече компилирано, и кодът не е наш, класическият вариант за разширяване на функционалността на типовете е чрез наследяване. Този подход може да стане доста сложен за осъществяване, тъй като навсякъде където се използват променливи от базовия тип, ще трябва да използваме променливи от наследяващия за да можем да достъпим нашата нова функционалност. За съжаление съществува и по-сериозен проблем. Ако типът, който искаме да наследим е маркиран с ключовата дума sealed, то опция за наследяване няма. Разширяващите методи (extension methods) решават точно този проблем – дават ни възможност да добавяме функционалност към съществуващ тип (клас или интерфейс), без да променяме оригиналния му код и дори без наследяване, т.е. работи също и с типове, които не подлежат на наследяване. Забележете, че чрез extension methods, можем да добавяме "имплементирани методи" дори към интерфейси.

Разширяващите методи се дефинират като статични методи в обикно­вени статични класове. Типа на първият им аргумент представлява класа (или интерфейса) към който се закачат. Преди него се слага ключовата дума this. Това ги отличава от другите статични методи и показва на компилатора, че това е разширяващ метод. Параметъра, пред който стои ключовата дума this, може да бъде използван в тялото на метода, за да се създаде функционалността на метода. Той реално представлява  обекта, с който разширяващия метод работи.

Разширяващите методи могат да бъдат използвани директно върху обекти от класа/интерфейса, който разширяват. Могат да бъдат извиквани и статично чрез статичния клас в който са дефинирани, но това не е препоръчителна практика.

clip_image002

За да могат да бъдат достъпени дадени разширяващи методи, трябва да бъде добавен с using съответния namespace, в който е дефиниран статичния клас описващ тези методи. В противен случай, компилаторът няма как да разбере за тяхното съществуване.

Разширяващи методи – примери

Нека вземем за пример разширяващ метод, който брои колко думи има в даден текст (string). Забележете, че типа string е sealed и не може да се наследява.

public static class StringExtensions

{

      public static int WordCount(this string str)

      {

            return str.Split(new char[] { ' ', '.', '?', '!'  },

                  StringSplitOptions.RemoveEmptyEntries).Length;

      }

}

Методът WordCount() се закача за класа string. Това е оказано с ключовата дума this преди типа и името на първия аргумент на метода (в случая str). Самият метод е статичен и е дефиниран в статичния клас StringExtensions. Използването на разширяващия метод става както всеки обикновен метод на класа string. Не забравяйте да добавите съответния namespace, в който се намира статичния клас, описващ разширяващите методи.

Пример:

static void Main()

{

      string helloString = "Hello, Extension Methods!";

      int wordCount = helloString.WordCount();

      Console.WriteLine(wordCount);

}

Самият метод се вика върху обекта helloString, който е от тип string. Методът получава обекта като аргумент и работи с него (в случая вика неговия метод Split(…) и връща броя елементи в получения масив).

Разширяващи методи за интерфейси

Освен върху класове, разширяемите методи могат да работят и върху интерфейси. Следващият пример взима обект от клас, който имплементира интерфейса списък от цели числа (IList<int>) и увеличава тяхната стойност с определено число. Самия метод IncreaseWidth(…) има достъп само до елементите, които се включват в интерфейса IList (например свойството Count).

public static class IListExtensions

{

      public static void IncreaseWidth(

            this IList<int> list,

            int amount)

      {

            for (int i = 0; i < list.Count; i++)

            {

                  list[i] += amount;

            }

      }

}

Разширяващите методи предоставят и възможност за работа върху generic типове. Нека вземем за пример метод, който обхожда с оператора foreach дадена колекция, имплементираща IEnumerable от произволен тип T:

public static class IEnumerableExtensions

{

      public static string ToString<T>(

            this IEnumerable<T> enumeration)

      {

            StringBuilder result = new StringBuilder();

            result.Append("[");

            foreach (var item in enumeration)

            {

                  result.Append(item.ToString());

                  result.Append(", ");

            }

            if (result.Length > 1)

                  result.Remove(result.Length - 2, 2);

            result.Append("]");

            return result.ToString();

      }

}

Пример за употребата на горните два метода:

static void Main()

{

      List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

      Console.WriteLine(numbers.ToString<int>());

      numbers.IncreaseWidth(5);

      Console.WriteLine(numbers.ToString<int>());

}

Резултатът от изпълнението на програмата ще е следният:

[1, 2, 3, 4, 5]

[6, 7, 8, 9, 10]

Анонимни типове (anonymous types)

В обектно-ориентираните езици (какъвто е C#) много често се налага да се дефинират малки класове с цел еднократно използване. Типичен пример за това е класа Point, съдържащ само 2 полета – координатите на точка. Създаването на обикновен клас само и единствено за еднократна употреба създава неудобство на програмистите и е свързано със загуба на време, особено за предефиниране на стандартните за всеки клас операции ToString(),Equals() и GetHashCode().

В езика C# има вграден начин за създаване на типове за еднократна употреба, наричани анонимни типове (anonymous types). Обектите от такъв тип се създават почти по същия начин, по който се създават стандартно обектите в C#. При тях не е нужно предварително да дефинираме тип данни за променливата. С ключовата дума var показваме на компилатора, че типа на променливата трябва да се разбере автоматично от дясната страна на присвояването. Реално нямаме и друг избор, тъй като дефинираме променлива от анонимен тип, на която не можем да посочим конкретно от кой тип е. След това пишем името на обекта, оператора равно и ключовата дума new. Във фигурни скоби изреждаме имената на свойствата на анонимния тип и техните стойности.

Анонимни типове – пример

Ето един пример за създаване на анонимен тип, който описва лек автомобил:

var myCar = new { Color = "Red", Brand = "BMW", Speed = 180 };

По време на компилация, компилаторът ще създаде клас с уникално име (например <>f__AnonymousType0), ще му създаде свойства (с getter и setter). В горния пример за свойствата Color и Brand компилаторът сам ще се досети, че са от тип string, а за свойството Speed, че е от тип int. Веднага след инициализацията си, обектът от анонимния тип може да бъде използван като обикновен тип с трите си свойства:

Console.WriteLine("My car is a {0} {1}.",

      myCar.Color, myCar.Brand);

Console.WriteLine("It runs {0} km/h.", myCar.Speed);

Резултатът от изпълнението на горния код ще е следния:

My car is a Red BMW.

It runs 180 km/h.

Още за анонимните типове

Както всеки друг тип в .NET и анонимните типове наследяват System.Object. По време на компилация, компилаторът ще предефинира вместо нас методите ToString(), Equals() и GetHashCode().

Console.WriteLine("ToString: {0}", myCar.ToString());

Console.WriteLine("Hash code: {0}",       myCar.GetHashCode().ToString());

Console.WriteLine("Equals? {0}", myCar.Equals(

      new { Color = "Red", Brand = "BMW", Speed = 180 }

));

Console.WriteLine("Type name: {0}", myCar.GetType().ToString());

Резултатът от изпълнението на горния код ще е следния:

ToString: { Color = Red, Brand = BMW, Speed = 180 }

Hash code: 1572002086

Equals? True

Type name: <>f__AnonymousType0`3[System.String,System.String,System.Int32]

Както може да се види от резултатa, методът ToString() e предефиниран така, че да изрежда свойствата на анонимния тип в реда, в който сме ги дефинирали при инициализацията на обекта (в случая myCar). Методът GetHashCode() е реализиран така, че да взима предвид всички полета и спрямо тях да изчислява собствена хеш-функция с малък брой колизии. Предефинираният от компилатора метод Equals(…) сравнява по стойност обектите. Както може да се види от примера, създаваме нов обект, който има абсолютно същите свойства като myCar и получаваме като резултат от метода, че новосъздаденият обект и старият са еднакви по стойност.

Анонимните типове, както и обикновените, могат да бъдат елементи на масиви. Инициализирането отново става с ключовата дума new като след нея се слагат квадратни скоби. Стойностите на масива се изреждат по начина, по който се задават стойности на анонимни типове. Стойностите в масива трябва да са хомогенни, т.е. не може да има различни анонимни типове в един и същ масив. Пример за дефиниране на масив от анонимни типове с 2 свойства (X и Y):

var arr = new[]   {     new { X = 3, Y = 5 },

                                                new { X = 1, Y = 2 },

                                                new { X = 0, Y = 7 }

                                          };

foreach (var item in arr)

{

      Console.WriteLine(item.ToString());

}

Резултатът от изпълнението на горния код ще е следния:

{ X = 3, Y = 5 }

{ X = 1, Y = 2 }

{ X = 0, Y = 7 }

Ламбда изрази (lambda expressions)

Ламбда изразите представляват анонимни функции, които съдържат изрази или последователност от оператори. Всички ламбда изрази използват ламбда оператора =>, който може да се чете като "отива в". Идеята за ламбда изразите в C# е взаимствана от функционалните езици (например Haskell, Lisp, Scheme, F# и др.). Лявата страна на ламбда оператора определя входните параметри на анонимната функция, а дясната страна представлява израз или последователност от оператори, която работи с входните параметри и евентуално връща някакъв резултат.

Обикновено ламбда изразите се използват като предикати или вместо делегати (променливи от тип функция), които се прилагат върху колекции, обработвайки елементите от колекцията по някакъв начин и/или връщайки определен резултат.

Ламбда изрази – примери

Например нека да разгледаме разширяващия метод FindAll(…), който може да се използва за отсяване на необходимите елементи. Той работи върху определена колекция, прилагайки ѝ даден предикат, който проверява всеки от елементите на колекцията дали отговаря на определено условие. За да го използваме, обаче, трябва да включим референция към библиотеката System.Core.dll и namespaceSystem.Linq, тъй като разширяващите методи върху колекциите се намират в този namespace.

Ако искаме например да вземем само четните числа от колекция с цели числа, можем да използваме метода FindAll(…) върху колекцията, като му подадем ламбда метод, който да провери дали дадено число е четно:

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6 };

List<int> evenNumbers = list.FindAll(x => (x % 2) == 0);

foreach (var num in evenNumbers)

{

      Console.Write("{0} ", num);

}

Console.WriteLine();

Резултатът е:

2 4 6

Горният пример обхожда цялата колекция от числа и за всеки елемент от нея (именуван x) се прави проверка дали числото се дели на 2 (с булевия израз (x % 2) == 0).

Нека сега разгледаме един пример, в който чрез разширяващ метод и ламбда израз ще създадем колекция, съдържаща определена информация от даден клас. В случая от класа куче - Dog (със свойства име Name и възраст Age), искаме да получим списък само с имената на кучетата. Това можем да направим с разширяващия метод Select(…)(дефиниран в namespace System.Linq), като му зададем за всяко куче x да го превръща в името на кучето (x.Name) и върнатия резултат (колекция) да запише в променливата names. С ключовата дума var казваме на компилатора сам да се определи типа на променливата по резултата, който присвояваме в дясната страна.

class Dog

{

      public string Name { get; set; }

      public int Age { get; set; }

}

 

static void Main()

{

      List<Dog> dogs = new List<Dog>()

      {

            new Dog { Name = "Rex", Age = 4 },

            new Dog { Name = "Sharo", Age = 0 },

            new Dog { Name = "Stasi", Age = 3 }

      };

      var names = dogs.Select(x => x.Name);

      foreach (var name in names)

      {

            Console.WriteLine(name);

      }

}

Резултатът е:

Rex

Sharo

Stasi

Използване на ламбда изрази с анонимни типове

С ламбда изрази можем да създаваме и колекции с анонимни типове, от колекция с някакви елементи. Например нека от колекцията dogs, съдържаща елементи от тип Dog да създадем нова колекция с елементи от анонимен тип, с 2 свойства – възраст и първата буква от името на кучето:

var newDogsList = dogs.Select(

      x => new { Age = x.Age, FirstLetter = x.Name[0] });

foreach (var item in newDogsList)

{

      Console.WriteLine(item);

}

Резултатът е:

{ Age = 4, FirstLetter = R }

{ Age = 0, FirstLetter = S }

{ Age = 3, FirstLetter = S }

Както може да се види от примера, новосъздадената колекция newDogsList е с елементи от анонимен тип, съдържащ свойствата Age и FirstLetter. Първият ред от примера може да се прочете така: създай ми променлива с неизвестен за сега тип, именувай я newDogsList и от dogs колекцията, за всеки неин елемент x създай нов анонимен тип с 2 свойства: Age, което е равно на свойството Age от елемента x и свойство FirstLetter, което пък е равно на първия символ от низа x.Name.

Сортиране чрез ламбда изрази

Ако искаме да сортираме елементите в дадена колекция, можем да изпол­зваме разширяващите методи OrderBy и OrderByDescending като им пода­дем чрез ламбда функция начина, по който да сортират елементите. Пример отново върху колекцията dogs:

var sortedDogs = dogs.OrderByDescending(x => x.Age);

foreach (var dog in sortedDogs)

{

      Console.WriteLine(string.Format(

            "Dog {0} is {1} years old.", dog.Name, dog.Age));

}

Резултатът е:

Dog Rex is 4 years old.

Dog Stasi is 3 years old.

Dog Sharo is 0 years old.

Оператори в ламбда изразите

Ламбда функциите могат да имат и тяло. До сега използвахме ламбда функциите само с един оператор. Сега ще разгледаме ламбда функции, които имат тяло. Да се върнем на примера с четните числа. За всяко число, към което се прилага нашата ламбда функция, искаме да отпечатаме на конзолата стойността му и да върнем като резултат дали е четно или не:

List<int> list = new List<int>() { 20, 1, 4, 8, 9, 44 };

// Process each argument with code statements

var evenNumbers = list.FindAll((i) =>

{

      Console.WriteLine("Value of i is: {0}", i);

      return (i % 2) == 0;

});

Value of i is: 20

Value of i is: 1

Value of i is: 4

Value of i is: 8

Value of i is: 9

Value of i is: 44

Ламбда изразите като делегати

Ламбда функциите могат да бъдат записани в променливи от тип делегат. Делегатите представляват специален тип променливи, които съдържат функции. Стандартните типове делегати в .NET са Action, Action<in T>, Action<in T1, in T2>, и т.н. и Func<out TResult>, Func<in T, out TResult>, Func<in T1, in T2, in TResult> и т.н. Типовете Func и Action са generic и съдържат типовете на връщаната стойност и типовете на параметрите на функциите. Променливите от тези типове са референции към функции. Ето пример за използването и присвояването на стойности на тези типове.

Func<bool> boolFunc = () => true;

Func<int, bool> intFunc = (x) => x < 10;

if (boolFunc() && intFunc(5))

{

      Console.WriteLine("5 < 10");

}

Резултатът е:

5 < 10

В горния пример дефинираме два делегата. Първият делегат boolFunc е функция, която няма входни параметри и връща като резултат от булев тип. На нея като стойност сме задали анонимна ламбда функция която не върши нищо и винаги връща стойност true. Вторият делегат приема като параметър променлива от тип int и връща булева стойност, която е истина, когато входния параметър x е по-малък от 10 и лъжа в противен случай. Накрая в if оператора викаме нашите два делегата, като на втория даваме параметър 5 и резултата от извикването им, както може да се види и на двата е true.

LINQ заявки (LINQ queries)

LINQ (Language-Integrated Query) представлява редица разширения на .NET Framework, които включват интегрирани в езика заявки и операции върху елементи от даден източник на данни (най-често масиви и колекции). LINQ e много мощен инструмент, който доста прилича на повечето SQL езици и по синтаксис и по логика на изпълнение. LINQ реално обработва колекциите по подобие на SQL езиците, които обработват редовете в таблици в база данни. Той е част от C# и VisualBasic синтаксиса и се състои от няколко основни ключови думи. За да използваме LINQ заявки в езика C#, трябва да включим референция към System.Core.dll и да добавим namespace-a System.Linq.

Избор на източник на данни с LINQ

С ключовите думи from и in се задават източникът на данни (колекция, масив и т.н.) и променливата, с която ще се итерира (обхожда) по колекцията (обхождане по подобие на foreach оператора). Например заявка, която започва така:

from culture

in CultureInfo.GetCultures(CultureTypes.AllCultures)

може да се прочете като: За всяка една стойност от колекцията CultureInfo.GetCultures(CultureTypes.AllCultures) задай име culture, и го използвай по-нататък в заявката…

Филтриране на данните с LINQ

С ключовата дума where се задават условията, които всеки от елементите от колекцията трябва да изпълнява, за да продължи да се изпълнява заявката за него. Изразът след where винаги е булев израз. Може да се каже, че с where се филтрират елементите. Например, ако искаме в предния пример да кажем, че ни трябват само тези от културите, чието име започва с малка латинска буква b, можем да продължим заявката с:

where culture.Name.StartsWith("b")

Както може да се забележи, след where…in конструкцията изпол­зваме само името, което сме задали за обхождане на всяка една променлива от колекцията. Ключовата дума where се компилира до извикване на extension метода Where().

Избор на резултат от LINQ заявката

С ключовата дума select се задават какви данни да се върнат от заяв­ката. Резултата от заявката е под формата на обект от съществуващ клас или анонимен тип. Върнатият резултат може да бъде и свойство на обектите, които заявката обхожда или самите обекти. Операторът select и всичко след него седи винаги в края на заявката. Ключовите думи from, in, where и select са достатъчни за създаването на проста LINQ заявка. Ето и пример:

List<int> numbers = new List<int>() {

      1, 2, 3, 4, 5, 6, 7, 8, 9, 10

};

var evenNumbers =

      from num in numbers

      where num % 2 == 0

      select num;

foreach (var item in evenNumbers)

{

      Console.Write(item + " ");

}

Резултатът е:

2 4 6 8 10

Горния пример прави заявка върху колекцията от числа numbers и записва в нова колекция само четните числа. Заявката може да се прочете така: За всяко число num от numbers провери дали се дели на 2 без остатък и ако е така го добави в новата колекция.

Сортиране на данните с LINQ

Сортирането чрез LINQ заявките се извършва с ключовата дума orderby. След нея се слагат условията, по които да се подреждат елементите, участващи в заявката. За всяко условие може да се укаже редът на подреждане: в нарастващ ред (с ключова дума ascending) или в намаляващ ред (с ключова дума descending) като по подразбиране се подреждат в нарастващ ред. Например, ако искаме да сортираме масив от думи по дължината им в намаляващ ред, можем да напишем следната LINQ заявка:

string[] words = { "cherry", "apple", "blueberry" };

var wordsSortedByLength =

      from word in words

      orderby word.Length descending

      select word;

foreach (var word in wordsSortedByLength)

{

      Console.WriteLine(word);

}

Резултатът е:

blueberry

cherry

apple

Ако не бъде указан начина, по който да се сортират елементите (т.е. ключовата дума orderby отсъства от заявката), се взимат елементите в реда, в който колекцията би ги върнала при обхождане с foreach оператора.

Групиране на резултатите с LINQ

С ключовата дума group се извършва групиране на резултатите по даден критерии. Форматът е следният:

group [име на променливата] by [признак за групиране] into [име на групата]

Резултатът от това групиране е нова колекция от специален тип, която може да бъде използвана по-надолу в заявката. След групирането, обаче, заявката спира да работи с първоначалната си променлива. Това означава, че в select може да се ползва само групата. Пример за групиране:

int[] numbers =

      { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0, 10, 11, 12, 13 };

int dividor = 5;

 

var numberGroups =

      from number in numbers

      group number by number % divisor into group

      select new { Remainder = group.Key, Numbers = group };

 

foreach (var group in numberGroups)

{

      Console.WriteLine(

      "Numbers with a remainder of {0} when divided by {1}:",

            group.Remainder, divisor);

      foreach (var number in group.Numbers)

      {

            Console.WriteLine(number);

      }

}

Резултатът е:

Numbers with a remainder of 0 when divided by 5:

5

0

10

Numbers with a remainder of 4 when divided by 5:

4

9

Numbers with a remainder of 1 when divided by 5:

1

6

11

Numbers with a remainder of 3 when divided by 5:

3

8

13

Numbers with a remainder of 2 when divided by 5:

7

2

12

Както може да се види от примера, на конзолата се извеждат числата, групирани по остатъка си от деление с 5. В заявката  за всяко число се смята number % divisor и за всеки различен резултат се прави нова група. По-надолу select оператора работи върху списъка от създадените групи и за всяка група създава анонимен тип, който съдържа 2 свойства: Remainder и Numbers. На свойството Remainder се присвоява ключа на групата (в случая остатъка от деление с divisor на числото). На свойството Numbers пък се присвоява колекцията group, която съдържа всички елементи в групата. Забележете, че select-а се изпълнява само и единствено върху списъка от групи. Там не може да се използва променливата number. По-натам в примера с 2 вложени foreach оператора се извеждат остатъците (групите) и числата, които имат остатъка (се намират в групата).

Съединение на данни с LINQ

Операторът join има доста по-сложна концепция от останалите LINQ оператори. Той съединява колекции по даден критерии (еднаквост) между тях и извлича необходимата информация от тях. Синтаксисът му е следният:

from [име на променлива от колекция 1] in [колекция 1]

join [име на променлива от колекция 2] in [колекция 2] on [част на условието за еднаквост от колекция 1] equals [част на условието за еднаквост от колекция 2]

По-надолу в заявката (в select-а например) може да се използва, както името на променливата от колекция 1, така и това от колекция 2. Пример:

public class Product

{

      public string Name { get; set; }

      public int CategoryID { get; set; }

}

public class Category

{

      public int ID { get; set; }

      public string Name { get; set; }

}

Резултатът е:

List<Category> categories = new List<Category>()

{

      new Category() { ID = 1, Name = "Fruit" },

      new Category() { ID = 2, Name = "Food" },

      new Category() { ID = 3, Name = "Shoe" },

      new Category() { ID = 4, Name = "Juice" },

};

List<Product> products = new List<Product>()

{

      new Product() { Name = "Strawberry", CategoryID = 1 },

      new Product() { Name = "Banana", CategoryID = 1 },

      new Product() { Name = "Chicken meat", CategoryID = 2 },

      new Product() { Name = "Apple Juice", CategoryID = 4 },

      new Product() { Name = "Fish", CategoryID = 2 },

      new Product() { Name = "Orange Juice", CategoryID = 4 },

      new Product() { Name = "Sandal", CategoryID = 3 },

};

var productsWithCategories =

      from product in products

      join category in categories

            on product.CategoryID equals category.ID

      select new {      Name = product.Name,

                                          Category = category.Name };

foreach (var item in productsWithCategories)

{

      Console.WriteLine(item);

}

Резултатът е:

{ Name = Strawberry, Category = Fruit }

{ Name = Banana, Category = Fruit }

{ Name = Chicken meat, Category = Food }

{ Name = Apple Juice, Category = Juice }

{ Name = Fish, Category = Food }

{ Name = Orange Juice, Category = Juice }

{ Name = Sandal, Category = Shoe }

В горния пример си създаваме два класа и мислена релация (връзка) между тях. На всеки продукт съответства някаква категория CategoryID (под формата на число), което отговаря на числото ID от класа Category в колекцията categories. Ако искаме да използваме тази релация и да си създадем нов анонимен тип, в който да запишем продуктите с тяхното име и името на тяхната категория, си пишем горната LINQ заявка. Тя свързва колекцията елементи от тип Category с колекцията от елементи от тип Product по споменатия признак (еднаквост между ID от Category и CategoryID от Products). В select частта на заявката използваме двете имена category и product, за да си конструираме анонимен тип с име на продукта и име на категорията.

Вложени LINQ заявки

В LINQ се поддържат и вложени заявки. Например последната заявка може да бъде написана чрез влагането на заявка в заявка по следния начин, като резултата е абсолютно същия както в заявката с join:

var productsWithCategories =

      from product in products

      select new {

            Name = product.Name,

            Category =

                  (from category in categories

                  where category.ID == product.CategoryID

                  select category.Name).First()

      };

Тъй като всяка LINQ заявка връща колекция от елементи (без значение дали резултатът от нея е с 0, 1 или няколко елемента), се налага използването на разширяващия метод First() върху резултата от вложе­ната заявка. Методът First() връща първия елемент (в нашия случай и единствен) от колекцията, върху която е приложен. По този начин получаваме името на категорията само по нейния ID номер.

Упражнения

1.     Имплементирайте разширяващ метод Substring(int index, int length) за класа StringBuilder, който връща нов StringBuilder и има същата функционалност като метода Substring(…) на класа String.

2.     Имплементирайте следните разширяващи методи за класовете, имплементиращи интерфейса IEnumerable<T>: Sum, Min, Max, Average.

3.     Напишете клас Student със следните свойства: първо име, фамилия и възраст. Напишете метод, който по даден масив от студенти намира всички студенти, на които името им е по-малко лексикографски от фамилията. Използвайте LINQ заявка.

4.     Напишете LINQ заявка, която намира първото име и фамилията на всички студенти, които са на възраст между 18 и 24 години включи­телно. Използвайте класа Student от предната задача.

5.     Като използвате разширяващите методи OrderBy(…) и ThenBy(…) с ламбда израз, сортирайте списък от студенти по първо име и по фамилия в намаляващ лексикографски ред. Напишете същата функционалност, използвайки LINQ заявка.

6.     Напишете програма, която отпечатва на конзолата всички числа в даден масив (или списък), които се делят едновременно на 7 и на 3. Използвайте вградените разширяващи методи с ламбда изрази и после напишете същото, само че с LINQ заявка.

7.     Напишете разширяващ метод на класа String, който прави главна, всяка буква, която е начало на дума в изречение на английски език. Например текстът "this iS a Sample sentence." трябва да стане на "This Is A Sample Sentence.".

Решения и упътвания

1.     Едно решение на задачата е да направите нов StringBuilder и в него да запишете символите с индекси започващи от index и с дължина length от обекта върху който ще работи разширяващият метод.

2.     Тъй като не всички класове имат предефинирани операторите + и /, операциите Sum и Average няма да могат да бъдат приложени директно върху тях. Един начин за справяне с този проблем е да конвертираме всеки обект към обект от тип decimal и после да извършим операциите върху тях. За конвертирането може да се използва статичният метод Convert.ToDecimal(…). За операциите Min и Max може да се зададе на темплейтния клас да наследява винаги IComparable, за да могат обектите да бъдат сравнявани.

3.     Прегледайте ключовите думи from, where и select от секцията LINQ заявки.

4.     Използвайте LINQ заявка, за да създадете анонимен тип, който съдържа само 2 свойства – FirstName и LastName.

5.     За LINQ заявката използвайте from, orderby, descending и select. За реализацията с ламбда изразите използвайте функциите OrderByDescending(…) и ThenByDescending(…).

6.     Вместо да правите 2 условия за where е достатъчно само да проверите дали числата се делят на 21.

7.     Използвайте метода ToLittleCase(…) на свойството TextInfo в култу­рата en-US по следния начин:

new CultureInfo("en-US", false).TextInfo.ToTitleCase(text);

Демонстрации (сорс код)

Изтеглете демонстрационните примери към настоящата глава от книгата: Ламбда-изрази-и-LINQ-заявки-Демонстрации.zip.

Дискусионен форум

Коментирайте книгата и задачите в нея във: форума на софтуерната академия.

Share
Тагове: , , , , , , , , , ,

Коментирай