Artykuł

maj 27 2012
0

Operacje na kolekcjach w C# z użyciem LINQ

Ponad dwa lata temu, popełniłem wpis na temat operacji na kolekcjach w C#. Post ten ku mej uciesze, wciąż cieszy się sporą popularnością, dlatego też postanowiłem napisać niejako jego kontynuację.

Gdy dwa lata temu pisałem tamten wpis, wciąż dominującą wersją frameworka .Net, była ta oznaczona wersją 2.0. Dziś sytuacja się trochę zmieniła i przynajmniej w moim odczuciu, coraz więcej osób korzysta z wersji 3.5 i 4.0, a na horyzoncie jest już .Net 4.5, który pojawi się w pełni wraz z Windowsem 8 i Visual Studio 11. Nowsze wersje frameworka wprowadziły wiele istotnych zmian w tym oczywiście tytułowy LINQ. Na temat LINQ pisałem już kilkukrotnie, dziś chciałbym jednak przedstawić wykorzystanie tej technologii w kontekście kolekcji, ponieważ takie połączenie zapewnia im właściwie drugie życie i zdecydowanie zwiększa ich użyteczność.

Klasa Person - obszar dla naszych manewrów

Podobnie jak w poprzednim wpisie, potrzebować będziemy jakiejś klasy, którą będziemy mogli użyć do budowania naszych kolekcji. Również w tym przypadku dobrze sprawdzi się klasa Person, którą w mniej lub bardziej podobnej postaci wykorzystywałem już w wielu innych wpisach na tym blogu.

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

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

Pole PersonId z założenia ma być unikalne - może to być np. pesel zapisany w postaci stringu, albo GUID. To stwierdzenie jest ważne z punktu widzenia dalszej części wpisu, gdzie będziemy przeprowadzać wyszukiwania.

Zabawy z listą

Lista w moim prywatnym odczuciu jest najpopularniejszą kolekcja, dlatego też postaram się jej poświęcić najwięcej uwagi. Swoją popularność zawdzięcza prostocie i elastyczności, a dzięki LINQ i jego możliwościom wyszukiwania z powodzeniem może zastąpić w wielu miejscach nawet słowniki.

Przed rozpoczęciem pracy, musimy zadeklarować użycie przestrzenie nazw LINQ, jeśli nie ma takiego wpisu:

using System.Linq;

Tworzenie listy

Na potrzeby naszych testów, utworzymy kolekcję obiektów Person, którą od razu wypełnimy danymi używając inicjalizatora obiektów:

List<Person> oPersonLists = new List<Person>()
{
    new Person("86010156123", "Jan", "Kowalski", 26),
    new Person("87010156123", "Anna", "Krawczyk", 25),
    new Person("88010156123", "Krzysztof", "Nowak", 24),
    new Person("85010156123", "Katrzyna", "Lewandowska", 27),
    new Person("84010156123", "Andrzej", "Wiśniewski", 28)
};

Standardowe zapytania LINQ

We wpisie przedstawiłem kilka przykładowych metod które można zastosować na takiej kolekcji obiektów oraz podstawowe zapytania. Przeanalizujmy teraz kilka przykładów dla przypomnienia:

  • Pobranie listy wszystkich osób za pomocą zapytania:
    var oSelectedPersons = from Person oPerson in oPersonLists
                           select oPerson;
  • Pobranie listy wszystkich osób, które mają przynajmniej 26 lat:
    var oSelectedPersons = from Person oPerson in oPersonLists
                           where oPerson.Age >= 26
                           select oPerson;
  • Pobranie listy wszystkich osób i posortowanie jej alfabetycznie według nazwiska:

    var oSelectedPersons = from Person oPerson in oPersonLists
                           orderby oPerson.LastName 
                           select oPerson;
  • Pobranie listy wszystkich osób, które mają więcej niż 24 lata i na imię Anna:
    var oSelectedPersons = from Person oPerson in oPersonLists
                           where oPerson.Age > 24 && oPerson.FirstName == "Anna"
                           select oPerson;

Metody LINQ dla kolekcji

Pisanie zapytań nie zawsze do końca jest wygodne, dlatego też opracowano szereg metod, które możemy wywołać na naszej kolekcji. W tym celu bardzo przydatne są wyrażenia lambda, które również już wcześniej opisywałem.

Metody rozszerzające kolekcje można również śmiało łączyć z samymi rezultatami LINQ, dzięki czemu uzyskujemy naprawdę potężne narzędzie. Spróbujmy teraz zapisać cztery przykłady z poprzedniego akapitu za pomocą odpowiednich metod:

  • Pobranie listy wszystkich osób. Cel ten sam w sobie mamy już osiągnięty już na starcie (poprzednio w gruncie rzeczy też tak było) i nie ma sensu zmieniać go za pomocą metody
  • Pobranie listy wszystkich osób, które mają przynajmniej 26 lat:
    var oSelectedPersons = oPersonsList.Where(oPerson => oPerson.Age >= 26);
  • Pobranie listy wszystkich osób i posortowanie jej alfabetycznie według nazwiska:

    var oSelectedPersons = oPersonsList.OrderBy(oPerson => oPerson.LastName);
  • Pobranie listy wszystkich osób, które mają więcej niż 24 lata i na imię Anna:
    var oSelectedPersons = oPersonsList.Where(oPerson => 
        (oPerson.FirstName == "Anna") && (oPerson.Age > 24));

Wyraźnie widać, że zapis z użyciem metod jest znacznie krótszy, a to tylko próbka, spójrzmy na kolejne przykłady jakie możemy zrealizować za pomocą metod:

  • Pobranie najstarszej osoby:
    var oSelectedPerson = oPersonsList.OrderByDescending(
        oPerson => oPerson.Age).Take(1);
  • Suma wieku wszystkich osób na liście:
    int nTotalAge = oPersonsList.Sum(oPerson => oPerson.Age);
  • Liczba osób pełnoletnich:
    int nAdultPersonsCount = oPersonsList.Count(oPerson => oPerson.Age > 18);
    Count bez podanego parametru podaje liczbę osób w kolekcji
  • Weryfikacja czy jakikolwiek Krzysztof znajduje się na liście:
    bool bIsKrzysztofOnList = oPersonsList.Any(
    	oPerson => oPerson.FirstName == "Krzysztof");
  • Średnia wieku wszystkich osób na liście
    double dAverageAge = oPersonsList.Average(oPerson => oPerson.Age);
  • Zwróć pierwszą osobę na liście, która ma ponad 26 lat
    Person oSelectedPerson = oPersonsList.First(oPerson => oPerson.Age > 26);
    Metoda First może być bardzo niebezpieczna, ponieważ w sytuacji gdy nie znajdzie ona dopasowania do warunku w niej zawartego, rzuci ona wyjątek. Osobiście bardziej preferuje metodę FirstOrDefault, która działa w identyczny sposób, tylko zwraca wartość null w sytuacji gdy nie znajdziemy interesującego nas rekordu. Należy oczywiście pamiętać o przetestowaniu wartości, czy jest różna od nulla

Powyższe punkty to tylko próbka zasadniczych możliwości. Rozwiązania mogą być bardziej złożone. Dla przykładu spróbujemy pobrać pierwszą osobę, której wiek równy jest średniej wszystkich osób w kolekcji:

Person oSelectedPerson = oPersonsList.FirstOrDefault(oPersonWithAveragedAge =>
    oPersonWithAveragedAge.Age == (oPersonsList.Average(oPerson => oPerson.Age)));

W rezultacie dla zadanego zestawu danych, powinniśmy otrzymać Jana Kowalskiego, który ma 26 lat, czyli dokładnie tyle ile wynosi średnia wszystkich osób na liście.

Zabawy ze słownikiem

W przypadku słownika sprawy wyglądają całkiem podobnie, tylko musimy wziąć poprawkę na inną budowę tej kolekcji. Dlatego też w tym przypadku kluczem naszej kolekcji będzie id osoby (pesel), a pole PersonId wyrzucimy z klasy Person, żeby nie dublować danych.

Tworzenie słownika

Słownik podobnie jak listę, utworzymy przy pomocy inicjalizatora obiektów:

Dictionary<string, Person> oPersonsDict = new Dictionary<string, Person>()
{
    {"86010156123", new Person("Jan", "Kowalski", 26)},
    {"87010156123", new Person("Anna", "Krawczyk", 25)},
    {"88010156123", new Person("Krzysztof", "Nowak", 24)},
    {"85010156123", new Person("Katrzyna", "Lewandowska", 27)},
    {"84010156123", new Person("Andrzej", "Wiśniewski", 28)}
};

Standardowe zapytania LINQ

Ze słownikiem możemy uzyskać bardzo podobne efekty jak z listą, aczkolwiek musimy zwrócić uwagę na pobieranie klucza/wartości słownika.

  • Pobranie listy wszystkich osób za pomocą zapytania:
    var oSelectedPersons = from Person oPerson in oPersonsDict.Values
                           select oPerson;
  • Pobranie listy wszystkich osób, które mają przynajmniej 26 lat:
    var oSelectedPersons = from Person oPerson in oPersonsDict.Values
                           where oPerson.Age > 26
                           select oPerson;
  • Pobranie listy wszystkich osób i posortowanie jej alfabetycznie według nazwiska:

    var oSelectedPersons = from Person oPerson in oPersonsDict.Values
                           orderby oPerson.LastName
                           select oPerson;
  • Pobranie listy wszystkich osób, które mają więcej niż 24 lata i na imię Anna:
    var oSelectedPersons = from Person oPerson in oPersonsDict.Values
                           where oPerson.Age > 24 && oPerson.FirstName == "Anna"
                           select oPerson;

Jak widać na powyższych przykładach, zapytania są w tym przypadku praktycznie identyczne jak w przypadku listy. Musieliśmy tylko zaznaczyć, że chodzi nam o wartości słownika.

Jeśli chcemy pobrać wszystkie klucze, musimy skorzystać z właściwości Keys:

var oSelectedPersons = from string sKey in oPersonsDict.Keys
                       select sKey;

Zaawansowane zapytania na słownikach

Dzięki specyficznej strukturze słowników, łatwo jest w tym przypadku się zagubić, z drugiej strony słowniki umożliwiają tworzenie naprawdę zaawansowanych zapytań. Spójrzmy na kilka przykładów:

  • Pobranie peseli wszystkich osób pełnoletnich na liście:
    var oSelectedPersons = from oElement in oPersonsDict
                           where oElement.Value.Age > 18
                           select oElement.Key;
  • Pobranie informacji o wszystkich osobach, których pesel zaczyna się od wartości 88:
    var oSelectedPersons = from oElement in oPersonsDict
                           where oElement.Key.StartsWith("88")
                           select oElement.Value;
  • Posortowanie wartości według peselu i zwrócenie nowej kolekjic:
    var oSelectedPersons = from oElement in oPersonsDict
                           orderby oElement.Key
                           select oElement;

Metody LINQ dla kolekcji

Dla słownika działają również oczywiście metody LINQ. Również w tym przypadku musimy jednak zwrócić uwagę na rozgraniczenie między kluczem i wartością.

  • Pobranie listy wszystkich osób (wartości):
    var oSelectedPersons = oPersonsDict.Select(oItem => oItem.Value);
  • Pobranie listy wszystkich osób, które mają przynajmniej 26 lat:
    var oSelectedPersons = oPersonsDict.Where(oPerson => oPerson.Value.Age >= 26);
  • Pobranie listy wszystkich osób i posortowanie jej alfabetycznie według nazwiska:

    var oSelectedPersons = oPersonsDict.OrderBy(oPerson => oPerson.Value.LastName);
  • Pobranie listy wszystkich osób, które mają więcej niż 24 lata i na imię Anna:
    var oSelectedPersons = oPersonsDict.Where(oPerson =>
        (oPerson.Value.FirstName == "Anna") && (oPerson.Value.Age > 24));

Dzięki zapytaniom LINQ oraz dedykowanym jemu metodom możemy uzyskać naprawdę wiele. Polecam własne eksperymenty w tej materii:)

Data ostatniej modyfikacji: 09.05.2013, 10:47.

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

Send to Kindle

Komentarze

blog comments powered by Disqus