Extension Methods, LINQ, Lambda expressions

Преди да вникна по-сериозно в горните 3 израза често ги ползвах по метода проба-грешка. Въпреки че вършеше работа, така едва ли щях да осъзная как работят преди да изпиша еквивалента на “Война и мир” в LINQ queries. В тази статия ще се постарая да обясня какво са те, как са свързани, като дам малко примери в тяхното използване. Сорса с примерите в гитхъб.

Чапаев се явява на изпит в академията. Навън го чака Петка.
– Какво стана, Чапаев, взе ли изпита?
– А бе, остави. Питаха ме колко прави 0,5 + 0,5. С цялата си душа чувствам, че е равно на литър, но не знам как да го изразя математически.

Extension methods

Language Integrated Query е технология, която ни позволява да обработваме колекции от данни. Всичко, което е в паметта, база данни или XML данни може да бъде сортирано, групирано, филтрирано и тн. чрез LINQ, стига да е в колекция, която имплементира интерфейса IEnumerable. Тези функционалности са предоставени чрез extension methods. 

Започнах много акъл да ръся и точно такива академични излияния няма да публикувам. С две думи – extension methods разширяват базовите способности на даден клас, без да са част от него, но докато пишем код на нас ни изглежда че са.

Един добър пример е разширяването на някоя структура като DateTime. Би било добре да имаме тип, който да може да ни каже колко работни дни остават до 1-ви когато примерно взимаме заплатата. За жалост в майкрософт не са се сетили за тази изключително важна функционалност, затова трябва сами да си я направим.


class AdvancedDateTime : DateTime
 {

}

Подобно наследяване в C# е невъзможно, защото структурите по подразбиране са sealed. Друг вариант е да направим статичен клас със статичен метод, който да взима като аргумент DateTime и да ни връща броя работни дни, но тогава ще пишем нещо от рода

AdvancedTime.WorkDaysToPayment(date);

С екстеншън можем да сведем писането до:

date.WorkDaysToPayment();

Сега получихме нов метод, който изглежда е от DateTime, но всъщност е дефиниран в напълно различен статичен клас. Понеже работим с време остават и малко имплементационни детайли – какво става ако го питаме в 22:00 и работният ден е минал? Какви проверки по-точно трябва да извършим за да разберем броят на работните дни? Ето дефиницията на самите методи –

public static class AdvancedDateTime
 {

 public static int WorkDaysToPayment(this DateTime date)
 {
   int daysToPayment = 0, currentMonth = date.Month;
   DateTime iteratingDate = date;

  if (date.IsPastWorkHours())
  {
     daysToPayment--;
  }

  while (iteratingDate.Month == currentMonth)
  {
     iteratingDate = iteratingDate.AddDays(1);
     if (!iteratingDate.IsWeekend())
     {
        daysToPayment++;
     }
  }

  return daysToPayment;
 }

 public static bool IsPastWorkHours(this DateTime date)
 {
   if (date.Hour > 17)
   {
     return true;
   }
   else
   return false;
 }

 public static bool IsWeekend(this DateTime date)
 {
   if (date.DayOfWeek == DayOfWeek.Sunday || date.DayOfWeek == DayOfWeek.Saturday)
   {
     return true;
   }
   else
     return false;
   }
 }

Пред всеки първи аргумент стои думата this, тя обозначава, че това е екстеншън метод за този тип данни, в случая DateTime. Първият метод прави проверката на оставащите работни дни до първи следващия месец, но освен него има и два други. Както се вижда не извършват някаква чак толкова незаменима роля – първият проверява дали работният ден е приключил, а вторият – дали сме в уикенд. Спокойно можех да ги напиша в WorkDaysToPayment, но всъщност така освен, че извеждаме методи за неща, които може да ни потрябват и в други ситуации, правим кода много по-четим. Ако проверката за уикенда беше по-сложна – проверяваха се специфични дати като празници, рожденни дни и други никому непотребни събития щеше да направи кода в WorkDaysToPayment по-трудно четим. Сега когато го преглеждаме, знаем на къде се проверява дали не е работен ден и къде, дали работният ден в подадената дата вече не е свършил. Ако искаме да видим имплементационните детайли с F12 се влиза в дефиницията на метод/клас.

Понеже екстеншъните нямат нужда да са част от даден клас, можем да си ги пишем без излишно да разширяват функционалността на класовете и да ги ползваме в по-специфични части от девелъпмънт процеса като тестове. В проектите Students и StudentsTests има типичен пример за това. Вместо да пишем нещо от рода на –

Student stud = new Student("foo", "fooEvich", "fooEvski",
 "lalaLand", 69, 911, "blooeweewew@kmal.com",
 "Obushtarstvo i tehnologii v ezoterichnata promishlenost",
 Specialty.Cleaner);

Възможно е с малко допълнителен код да направим ясно какви точно стойности се инициализират

Student stud = new Student()
 .AddThreeNames("lala", "papa", "fafa")
 .AddSSN(356444)
 .AddAddress("sesame street")
 .AddCourse("Traktorizam")
 .AddSpecialty(Specialty.SoftwareArchitechure);

Basic LINQ

Общото между екстеншъните и LINQ, е че операторите в LINQ се правят точно чрез екстеншън методи. Вместо да се добавят нови и нови методи в интерфейса IEnumerable,  които после трябва да имплементираме в наследниците, майкрософт са предпочели по-гъвкавият вариант. В проекта Students_LINQ имаме лист от гръцки герои, които са решили, че е време да получат ‘вишу’ и са записали. Правим query – кои са героите, които подредени по първа буква са по-големи от дадената. Дефиницията на метода –

public static IEnumerable<Student> GetNamesAfterChar(this IEnumerable<Student> students, char ch)
 {
     foreach (var stud in students)
     {
         if (stud.FirstName.ToLower()[0] > ch)
         {
             yield return stud;
         }
     }
 }

Въртим в цикъл колекцията IEnumerable<Student> и за всеки обект проверяваме, дали  първата буква във FirstName е по-голяма лексикографски от дадената ch. Ако е – я добавяме в нова колекция IEnumerable<Student>, която вече връщаме на foreach цикъла в Main метода на програмата. Там се циклят само студентите (с имена на гръцки герои), чиято първа буква е по-голяма от ‘i’. Всички линк оператори приличат на горния и се пишат като екстеншън методи. Проблемът на нашият, е че работи само със студенти. Това, което искаме да имаме е оператор, който да може да си върши работата за колекции от типа IEnumerable<Всичко> или казано на компетентен език – да е по-generic.

Lambda expressions

Невъзможно е да предвидим какво би могло да се сортира в IEnumerable<T>, няма как да очакваме всеки обект да има FirstName или каквото и да е друго конкретно поле. Така, че когато имплементираме линк оператори нашата задача е да напишем как се филтрират колекциите, а този, който ги ползва трябва да ни каже кои. Това става, чрез ламбда функции и делегата Func<>. С няколко малки промени предишният метод може да се промени, така че да работи при всеки тип –

public static IEnumerabl<T> LexicographicallyBigger<T>(this IEnumerable<T> objects, char ch, Func<T, string> predicate)
 {
     foreach (var item in objects)
     {
         if (predicate(item).ToLower()[0] > ch)
         {
             yield return item;
         }
     }
 }

Заменихме IEnumerable<Student> с IEnumerable<T>. Добавихме и втори аргумент Func<T, string>. Func е делегат и в ъгловите скоби показваме каква е неговата сигнатура – всички са аргументи, а последният е типа, който трябва да връща метода. В нашия случай Func е делегат, който приема като аргумент T и връща string.

students.LexicographicallyBigger('i', x => x.FirstName)

Това е кодът по извикването. Даваме буквата спрямо която искаме да сравняваме и пишем ламбда експрешън, който взима като аргумент x(което е от тип student, погледнете дефиницията на метода) и връща стринг.

И за да затвърдим нещата, ще напишем нещо извратено. Нов оператор, който ще сортира лексикографски по първата буква, ако минутата в подаденият DateTime е четна и по втората, ако е нечетна –

public static IEnumerable<T> ThisWillHurt<T, DateTime>(this IEnumerable<T> objects, char ch, DateTime date, Func<T, DateTime, string> predicate)
 {
      foreach (var item in objects)
      {
          if (predicate(item, date).ToLower()[0] > ch)
          {
              yield return item;
          }
      }
 }

Дефиницията всъщност не изглежда много сбъркана. Това е защото там казваме как, а ето и кои

IEnumerable<Student> notPersistantQuery = students.ThisWillHurt('i', DateTime.Now,
 (x, date) => {
if ((date.Minute & 1) == 0)
{
return x.FirstName;
}
else
return x.FirstName[1].ToString();
 });

Final Words

С тези примери целя най-вече да покажа основни начини за боравене с тези технологии и тяхната връзка, защото имам чувството, че много ги използват без разбиране, понеже и аз правех така. Не съм запознат с абсолютно всички възможности на linq и едва ли някога ще бъда. В msdn има архив с много примери и ако се интересувате можете да му хвърлите едно око – пльок.

Posted in Programming Tagged with: , , , , , , , , , , ,

Leave a Reply

Your email address will not be published.