Artykuł

wrz 29 2010
0

LINQ to Object w praktyce

Niejednokrotnie tworząc własne kolekcje danych, chcielibyśmy mieć swobodę ich edycji, filtrowania i przeszukiwania. Podstawowe operacje dostarczone przez interfejs kolekcji, nie rozpieszczają użytkowników. W takim momencie, każdy programista z zazdrością spogląda na kolegę, który zajmuje się bazą danych i który dzięki zapytaniom, może właściwie w dowolny sposób manipulować posiadanymi danymi. Jednak jeśli jesteś programistą .Net i korzystasz z tego Frameworka w wersji co najmniej 3.5 - to mam dla Ciebie dobrą wiadomość, a jest nią technologia LINQ. Zanim jednak zasiądziemy do LINQ, przygotujemy nieśmiertelną klasę Person, na której będziemy eksperymentować, zapoznamy się ze zmiennymi i obiektami deklarowanymi za pomocą słowa kluczowego var oraz poznamy wyrażenia lambda.

Klasa Person

Ponieważ potrzebujemy obiektów, a obiekty robi się z klas, a jeśli mowa o klasach, to na pewno przyda się niezastąpiona klasa Person. Dziś rozszerzymy ją o dodatkowe pole będące numerem telefonu. Myślę, że kod nie wymaga żadnych wyjaśnień.

public class Person
{
    public int PersonId { get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public int Age { get; private set; }
    public int Phone { get; private set; }

    public Person(int nPersonId, string sFirstName, string sLastName, 
        int nAge, int nPhone)
    {
        PersonId = nPersonId;
        FirstName = sFirstName;
        LastName = sLastName;
        Age = nAge;
        Phone = nPhone;
    }
}

Kilka zdań o słowie kluczowym var

Programista języka słabo typowanego, nigdy nie musi się martwić o charakter tworzonej zmiennej, bo język robi to za niego. Zmienna taka po prostu jest. Raz jest intem, potem stringiem czy boolem. Inne perypetie, czekają programistów języków silnie typowany, czyli choćby .Netowców, czyli docelowych adresatów tego wpisu. Tutaj nie dość, że każda zmienna musi mieć z góry określony typ, to o zgrozo nie można tego już później zmienić w trakcie działania programu! Brzmi to oczywiście strasznie, jednak każdy wytrawny programista, szybko docenia zalety silnego typowania. Jednak czasem, przydałaby się możliwość podstawienia do zmiennej dowolnego typu danych, bez jawnego określenia tego typu przez nas samych, a cała odpowiedzialność została by zrzucona na biedny kompilator. Co w takim przypadku zrobić?

Jest tutaj pewne rozwiązanie. Jeśli jesteś szczęśliwym koderem .Neta w wersji przynajmniej 3.0, to możesz wykorzystać tzw. implicity-typed variables czyli zmienne słabo typowane, które deklarujemy przy pomocy słowa kluczowego var.

Jeśli już zacieracie ręce, myśląc aby zrobić z C# drugi PHP, to muszę Was rozczarować. Zmienne typu var, zostają na zawsze zmienną raz nadanego typu i zachowują cechy tego typu. Spójrzmy jak to działa w praktyce:

class Program
{
    static void Main(string[] args)
    {
        var nNumber = 5;
        var sString = "Ala ma kota, a kot ma Alę.";
        var bBoolean = true;
        if (bBoolean && (nNumber > 0))
        {
            Console.WriteLine(sString.Substring(0, 11));
        }
        Console.ReadKey();
    }
}

Prawda, że piękny kod? Nie użyłem tutaj ani jednego typu danych, a mimo to aplikacja kompiluje się i na dodatek działa prawidłowo. Jak myślicie jaki będzie wyniki działania tego bardzo krótkiego programu?

Oczywiście mimo, że ten kod działa i działa dobrze, to nie znaczy to, że teraz mamy wszystkie zmienny i obiekty deklarować za pomocą słowa kluczowego var. Zawsze powinno być one ostatecznością. Prawdziwie przydatne okaże się ono w przypadku wykorzystania technologii LINQ.

Wyrażenia lambda

Wyrażenia lambda, są kolejną nowością .Net Frameworka w wersji 3.0. Wykorzystują one nowy operator i pełnią rolę podobną do metod anonimowych. Wyrażenia lambda, występują zasadniczo w dwóch postaciach:

(dane_wejsciowe) => wartosc
(dane_wejsciowe) => { instrukcja; }

I dla przykładu:

(string sName) => { Console.WriteLine("Witaj " + sName); };
(int nNumber) => nNumber * 5;

Jednak, samo wyrażenie w takiej postaci nie zadziała. Powinniśmy opakować je w delegat:

class Program
{
    delegate void DWriteHello(string sName);
        
    static void Main(string[] args)
    {
        DWriteHello DWriteHello = (string sName) => 
            { Console.WriteLine("Witaj " + sName); };
        DWriteHello("Jurek");
        Console.ReadKey();
    }
}

Myślę, że sam kod jak i rezultat nie wymagają większych wyjaśnień;)

Dane testowe

Mamy już klasę Person, znamy słowo kluczowe var, czas więc przygotować jakieś dane testowe. W tym celu, utworzymy listę obiektów klasy Person:

List<Person> oPersonsList = new List<Person>();
oPersonsList.Add(new Person(1, "Jan", "Kowalski", 31, 61423423));
oPersonsList.Add(new Person(2, "Adam", "Nowak", 28, 22126621));
oPersonsList.Add(new Person(3, "Anna", "Kowalczyk", 30, 22532765));
oPersonsList.Add(new Person(4, "Katarzyna", "Nowaczyk", 26, 62323545));
oPersonsList.Add(new Person(5, "Adam", "Nowakowski", 17, 38434213));

Zalecam użycie takich właśnie danych w dalszej części tego wpisu.

LINQ - z czym to się je?

LINQ jest akronimem angielskiego zwrotu Language Integrated Query co w wolnym tłumaczeniu znaczy Zapytanie zintegrowane z językiem programowania. Ta nowa technologia, wprowadzona w .Net w wersji 3.5, pozwala zunifikować sposób selekcji i filtrowania danych z wielu różnych, kompletnie odmiennych źródeł. W tym celu, wykorzystujemy m. in. język T-SQL (używany powszechnie w relacyjnych bazach danych), zmienne deklarowane za pomocą słowa kluczowego var, czy też wyrażenia lambda.

Dzięki LINQ, możemy wyciągać dowolne dane z naszej listy osób, filtrować je, budować zapytania, łączyć te dane w spójną i logiczną całość, możliwości są naprawdę spore i tak na dobrą sprawę, ograniczyć może nas tylko wyobraźnia (może tu się trochę rozpędziłem:P).

LINQ występuje w kilku podstawowych postaciach. Poza tytułowym LINQ to Object istnieją również:

  • LINQ to ADO.NET
  • LINQ to SQL Server
  • LINQ to XML

Nic nie stoi również na przeszkodzie, by utworzyć własne źródło danych LINQ:)

Podstawowa selekcja danych

Czas przejść jednak do praktyki. Zaraz się przekonamy, jak szybko można wyselekcjonować dowolną grupę osób, według określonych kryteriów. Co trzeba w tym celu zrobić? Po prostu wykorzystać zapytanie typu SELECT w następującej postaci:

from TYP_OBIEKTU in KOLEKCJA_OBIEKTOW 
where WARUNEK 
orderby PORZADEK 
select TYP_OBIEKTU

Na pierwszy rzut oka, wygląda to nieco dziwacznie. Wszystko powinno się jednak rozjaśnić po dwóch przykładowych zapytaniach. Oto pierwsze z nich:

var oAdultPersonsList = from Person in oPersonsList
                        where Person.Age >= 18
                        orderby Person.Age
                        select Person;

Co będzie efektem tego zapytania? Spójrzmy po kolei. Na początku z listy osób, wyciągamy wszystkie obiekty klasy Person, dla których spełniony jest warunek, że wiek wynosi przynajmniej 18 lat. Następnie, tak otrzymaną listę wyników sortujemy według wieku i dla każdego wyciągniętego rekordu wybieramy obiekt klasy Person. Wszystko to, zapisujemy do obiektu oAdultPersonsList, który odpowiedni zadeklaruje już za nas kompilator. Choć równie dobrze, moglibyśmy zrobić to sami, deklarując listę obiektów Person.

Spójrzmy teraz na drugi przykład. Tym razem, chcemy wyciągnąć listę wszystkich Adamów. Nic prostszego:

var oListOfAdams = from Person in oPersonsList 
                    where Person.FirstName == "Adam" 
                    orderby Person.LastName
                    select Person;

Zapytanie jest bardzo podobne do poprzedniego. Oczywiście zmieniliśmy tutaj klauzulę where oraz porządek sortowania (tym razem robimy to po nazwisku). W rezultacie, powinniśmy otrzymać listę zawierającą dwóch osobników.

Jak widać, LINQ łączy elastyczność znaną dotychczas z baz danych z możliwościami drzemiącymi w języku programowania.

Kalkulowanie

Jednym z ciekawszych aspektów baz danych od zawsze była możliwość kalkulowania. Mogliśmy np. wyciągać wartość minimalną, maksymalną, średnią itp. Podobne funkcje oferuje nam również LINQ. Spójrzmy na poniższy listing, na którym operować będziemy na danych dotyczących tylko pełnoletnich osób (powinny być cztery takie osoby):

Console.WriteLine("Na liście nazwisk zawierających " + 
    oPersonsList.Count + " osób," + " znaleziono " + 
    oAdultPersonsList.Count() + " pełnoletnich osób");
Console.WriteLine("Średni wiek, wszystkich pełnoletnich osób to " + 
    oAdultPersonsList.Average(oPerson => oPerson.Age) + " lat");
Console.WriteLine("Najstarsza z pełnoletnich osób ma " + 
    oAdultPersonsList.Max(oPerson => oPerson.Age) + " lat");
var oYoungestPersonDetailed = oAdultPersonsList.Single(oPerson => 
    (oPerson.Age == oAdultPersonsList.Min(oYoungestPerson => 
        oYoungestPerson.Age)));
Console.WriteLine("Najmłodsza z pełnoletnich osób ma " + 
    oYoungestPersonDetailed.Age + " lat i nazywa się " +
    oYoungestPersonDetailed.FirstName + " " + 
    oYoungestPersonDetailed.LastName);

Przeanalizujmy teraz po kolei co tu się właściwie dzieje. W liniach 1-3, zaprezentowane zostało użycie metody Count, która zwraca liczbę rekordów analizowanej kolekcji.

W liniach 4-5, została zaprezentowana metoda Average. Ustala ona średnią wartość badanej cechy (wartość powinna być wyrażona liczbowo). Konieczne w tym przypadku, jest wykorzystanie wyrażenia lambda do wskazania badanej cechy w obiekcie klasy Person.

W liniach 6-7, sytuacja analogiczna. Tym razem, pobieramy maksymalną wartość - procedura Max.

Coś specjalnego zostawiłem na koniec. W tym przypadku, nie pobierzemy po prostu minimalnej wartości za pomocą procedury Min. Zrobimy coś lepszego. Pobierzemy dane osoby, która jest najmłodsza i wykorzystamy do tego metodę Single. Metoda Single jak sama nazwa wskazuje, pobiera pojedynczą wartość. Czyli jeśli osób klasyfikujących się do tego warunku będzie więcej, to ujrzymy bardzo przyjemny wyjątek.

Wróćmy jednak do kodu. Jeśli spojrzymy na niego uważnie, to odnajdziemy te samą konstrukcję znaną z metod Average oraz Max zagnieżdżoną wewnątrz kolejnej konstrukcji. Idąc od końca, pobieramy najpierw najniższy wiek (pamiętajmy, że dotyczy on osób pełnoletnich, czyli będzie to 26 lat), a następnie pobieramy dane pojedynczej osoby, która pasuje do tego zapytania. Osoby biegłe w bazach danych, poczują się tutaj na pewno bardzo swojsko:)

Łączenie zbiorów danych

W LINQ możemy łączyć zbiory danych na wiele różnych sposobów. Spójrzmy na kilka metod rozszerzających pozwalających na wykonywanie szeregu operacji na zbiorach:

var oListOfAdultsAndAdams = oAdultPersonsList.Concat(oListOfAdams);
var oListOfAdultsAndAdams2 = oAdultPersonsList.Union(oListOfAdams).Distinct();
var oListOfAdultAdams = oAdultPersonsList.Intersect(oListOfAdams);
var oListOfUnderAgeAdams = oAdultPersonsList.Except(oListOfAdams);

W pierwszych dwóch liniach, wykonujemy operację łączenia - metody Concat i Union. Takie połączenie, może doprowadzić do powstania duplikatów (i na pewno tak będzie!), dlatego w drugim przypadku użyjemy metody Distinct, który usunie duplikaty.

W linii trzeciej, widoczna jest metoda Intersect, która wyznacza część wspólną obu zbiorów. Czyli będzie to np. pełnoletni Adam.

W ostatniej linii, wyznaczamy różnicę zbiorów. Czyli będzie to, niepełnoletni Adam.

Jeśli chcielibyśmy teraz obejrzeć zawartość jednej z wyselekcjonowanych kolekcji, powinniśmy skorzystać z następującej konstrukcji:

Console.WriteLine("Lista pełnoletnich Adamów: ");
foreach (var oAdultAdam in oListOfAdultAdams)
{
    Console.WriteLine(oAdultAdam.FirstName + " " + oAdultAdam.LastName);
}

Czyli notabene, nic nowego - klasyczny foreach.

Łączenie różnych zbiorów danych z wykorzystaniem join

Ostatnią rzeczą, którą chciałbym zaprezentować to możliwość łączenia dwóch zbiorów, za pomocą operatora join, który jest bardzo popularny w relacyjnych bazach danych. Aby to zrobić, powinniśmy przygotować dwa zestawy danych:

var oListOfPeople = from Person in oPersonsList
                    select new { Person.PersonId, Person.FirstName, Person.LastName };
var oListOfTelephones = from Person in oPersonsList
                        select new { Person.PersonId, Person.Phone };

Zasadniczo moglibyście spytać teraz, po co komplikować życie, przecież można to zrobić jednym zapytaniem, skoro wszystko jest w tej samej tabeli i macie też rację, ale jako, że jest to proste źródło danych, to świetnie nadaje się do zobrazowania joina:

var oPersonsWithTelephones = from Telephone in oListOfTelephones
                                join People in oListOfPeople
                                on Telephone.PersonId equals People.PersonId
                                select new
                                {
                                    People.PersonId,
                                    People.FirstName,
                                    People.LastName,
                                    Telephone.Phone
                                };

Jak widać, tutaj sprawy się już trochę bardziej komplikują. Ale nie ma strachu:) Po pierwsze, zwracamy tutaj na dynamicznie tworzone typy za pomocą var. Mamy tutaj np. typ People, czy Telephone, których nazwy po prostu wymyśliłem, a które zawierają tymczasową zawartość. Również zwrócony typ danych jest dynamiczny. No i najważniejsze, połączenie operatorem join, dokonywane za pomocą wartości id. Warto zapoznać się, że składnią tego polecenia i zwrócić uwagę na użyte słowa kluczowe.

Efektem końcowym, jest lista nowych, dynamicznie utworzonych obiektów, którą możemy wyświetlić w tradycyjny już sposób:

Console.WriteLine("Lista osób:");
foreach (var oPeople in oPersonsWithTelephones)
{
    Console.WriteLine("Imię: {0}, Nazwisko: {1}, Telefon: {2}.",
        oPeople.FirstName, oPeople.LastName, oPeople.Phone);
}

Cały zaprezentowany tutaj materiał to tylko wycinek możliwości jakie daje nam LINQ, jednakże myślę że jest on na tyle klarowny i pełny, że powinien on Was zachęcić do tej technologii:)

Wpis powstał dzięki treściom zawartym w książce "C#3.0 i NET 3.5 Technologia LINQ" autorstwa Jacka Matulewskiego oraz wiedzy i praktyce własnej.

Data ostatniej modyfikacji: 05.04.2012, 14:02.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus