Łączenie kolekcji obiektów przy użyciu LINQ

2012-06-28

W dzisiejszym wpisie zajmę się tematem łączenia kolekcji obiektów przy użyciu mechanizmów dostępnych w LINQ. Pokażę przykłady zastosowań metod Concat, Union, Intersect, Except, Zip oraz klauzuli Join (zarówno dla złączeń wewnętrznych jak i zewnętrznych). Dla każdego przykładu złączenia kolekcji przedstawię analogiczny sposób łączenia zbiorów danych w języku SQL. Na początku konieczne jest przygotowanie tabel i danych testowych w języku SQL oraz ich odpowiedników w postaci klas i kolekcji obiektów w C#.

Poniższy skrypt przygotowuje trzy tabele: #F1Teams (wybrane zespoły Formuły 1), #DriversChampions (kierowcy startujący w sezonie 2012, posiadający tytuł mistrza świata), #DriversWinners (kierowcy, którzy wygrali przynajmniej jeden wyścig w sezonie 2012):

create table #F1Teams
(
    Name varchar(50) not null,
    Country varchar(50) not null
)

create table #DriversChampions
(
    FirstName varchar(50) not null,
    LastName varchar(50) not null,
    Team varchar(50) not null
)

create table #DriversWinners
(
    FirstName varchar(50) not null,
    LastName varchar(50) not null,
    Team varchar(50) not null
)

insert into #F1Teams (Name, Country)
values
    ('Ferrari', 'Włochy'),
    ('McLaren', 'Wielka Brytania'),
    ('Mercedes', 'Niemcy'),
    ('Red Bull', 'Austria'),
    ('Williams', 'Wielka Brytania')

insert into #DriversChampions (FirstName, LastName, Team)
values
    ('Fernando', 'Alonso', 'Ferrari'),
    ('Jenson', 'Button', 'McLaren'),
    ('Lewis', 'Hamilton', 'McLaren'),
    ('Kimi', 'Raikkonen', 'Lotus'),
    ('Michael', 'Schumacher', 'Mercedes'),
    ('Sebastian', 'Vettel', 'Red Bull')

insert into #DriversWinners (FirstName, LastName, Team)
values
    ('Fernando', 'Alonso', 'Ferrari'),
    ('Jenson', 'Button', 'McLaren'),
    ('Lewis', 'Hamilton', 'McLaren'),
    ('Pastor', 'Maldonado', 'Williams'),
    ('Nico', 'Rosberg', 'Mercedes'),
    ('Sebastian', 'Vettel', 'Red Bull'),
    ('Mark', 'Webber', 'Red Bull')

select * from #F1Teams
select * from #DriversChampions
select * from #DriversWinners

Poniższy kod tworzy klasy F1Team i Driver oraz kolekcje obiektów odpowiadające utworzonym wcześniej tabelom w bazie danych:

class F1Team
{
    public string Name { get; set; }
    public string Country { get; set; }

    public F1Team(string name, string country)
    {
        Name = name;
        Country = country;
    }
}

class Driver
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Team { get; set; }

    public Driver(string firstName, string lastName, string team)
    {
        FirstName = firstName;
        LastName = lastName;
        Team = team;
    }
}

List<F1Team> F1Teams;
List<Driver> DriversChampions;
List<Driver> DriversWinners;

private void PrepareData()
{
    F1Teams = new List<F1Team>
    {
        new F1Team("Ferrari", "Włochy"),
        new F1Team("McLaren", "Wielka Brytania"),
        new F1Team("Mercedes", "Niemcy"),
        new F1Team("Red Bull", "Austria"),
        new F1Team("Williams", "Wielka Brytania")
    };

    DriversChampions = new List<Driver>
    {
        new Driver("Fernando", "Alonso", "Ferrari"),
        new Driver("Jenson", "Button", "McLaren"),
        new Driver("Lewis", "Hamilton", "McLaren"),
        new Driver("Kimi", "Raikkonen", "Lotus"),
        new Driver("Michael", "Schumacher", "Mercedes"),
        new Driver("Sebastian", "Vettel", "Red Bull")
    };

    DriversWinners = new List<Driver>
    {
        new Driver("Fernando", "Alonso", "Ferrari"),
        new Driver("Jenson", "Button", "McLaren"),
        new Driver("Lewis", "Hamilton", "McLaren"),
        new Driver("Pastor", "Maldonado", "Williams"),
        new Driver("Nico", "Rosberg", "Mercedes"),
        new Driver("Sebastian", "Vettel", "Red Bull"),
        new Driver("Mark", "Webber", "Red Bull")
    };
}

class DriverComparer : IEqualityComparer<Driver>
{
    public bool Equals(Driver d1, Driver d2)
    {
        if (Object.ReferenceEquals(d1, d1))
            return true;

        if ((d1 == null) || (d2 == null))
            return false;

        return d1.FirstName == d2.FirstName &&
            d1.LastName == d2.LastName &&
            d1.Team == d2.Team;
    }

    public int GetHashCode(Driver d)
    {
        if (d == null)
            return 0;

        int hashFirstName = (d.FirstName == null) ? 0 : d.FirstName.GetHashCode();
        int hashLastName = (d.LastName == null) ? 0 : d.LastName.GetHashCode();
        int hashTeam = (d.Team == null) ? 0 : d.Team.GetHashCode();

        return hashFirstName ^ hashLastName ^ hashTeam;
    }
}

Dodatkowo utworzona została klasa DriverComparer implementująca interfejs IEqualityComparer<T>. Zawiera ona metody służące do porównania ze sobą dwóch obiektów danego typu (w tym przypadku klasy Driver). Obiekt klasy DriverComparer wykorzystywany będzie przy metodach Union, Intersect oraz Except w celu stwierdzenia, czy wybrane elementy dwóch kolekcji są sobie równe.

Concat (Union all)

Połączenie danych z tabel #DriversChampions i #DriversWinners:

select FirstName, LastName, Team from #DriversChampions
union all
select FirstName, LastName, Team from #DriversWinners

Połączenie elementów z kolekcji DriversChampions i DriversWinners:

var ChampionsConcatWinners = DriversChampions.Concat(DriversWinners);

Union

Połączenie danych z tabel #DriversChampions i #DriversWinners z eliminacją duplikatów (wyniki zawierają tylko unikalne dane):

select FirstName, LastName, Team from #DriversChampions
union
select FirstName, LastName, Team from #DriversWinners

Połączenie elementów z kolekcji DriversChampions i DriversWinners z eliminacją duplikatów (wynikowa kolekcja zawiera tylko unikalne dane, w celu porównania obiektów z dwóch kolekcji wykorzystany został obiekt klasy DriverComparer):

var ChampionsUnionWinners = DriversChampions.Union(DriversWinners, new DriverComparer());

lub:

var ChampionsUnionWinners =
    DriversChampions.Concat(DriversWinners).Distinct(new DriverComparer());

Intersect

Zwrócenie wspólnych danych z tabel #DriversChampions i #DriversWinners:

select FirstName, LastName, Team from #DriversChampions
intersect
select FirstName, LastName, Team from #DriversWinners

Zwrócenie wspólnych elementów z kolekcji DriversChampions i DriversWinners:

var ChampionsIntersectWinners =
    DriversChampions.Intersect(DriversWinners, new DriverComparer());

Except

Zwrócenie danych z tabeli #DriversChampions nie występujących w tabeli #DriversWinners:

select FirstName, LastName, Team from #DriversChampions
except
select FirstName, LastName, Team from #DriversWinners

Zwrócenie elementów z kolekcji DriversChampions nie występujących w kolekcji DriversWinners:

var ChampionsExceptWinners =
    DriversChampions.Except(DriversWinners, new DriverComparer());

Join (Inner join)

Złączenie tabel #DriversChampions i #F1Teams:

select d.FirstName, d.LastName, d.Team, t.Country
from #DriversChampions d
    inner join #F1Teams t on t.Name = d.Team

Złączenie kolekcji DriversChampions i F1Teams:

var DriversTeams = from d in DriversChampions
                   join t in F1Teams on d.Team equals t.Name
                   select new
                   {
                       FirstName = d.FirstName,
                       LastName = d.LastName,
                       Team = d.Team,
                       Country = t.Country
                   };

Join (Left join)

Złączenie zewnętrzne tabel #DriversChampions i #F1Teams:

select d.FirstName, d.LastName, d.Team, t.Country
from #DriversChampions d
    left join #F1Teams t on t.Name = d.Team

Złączenie zewnętrzne kolekcji DriversChampions i F1Teams:

var DriversTeams = from d in DriversChampions
                   join t in F1Teams on d.Team equals t.Name
                   into TempF1Teams
                   from tt in TempF1Teams.DefaultIfEmpty()
                   select new
                   {
                       FirstName = d.FirstName,
                       LastName = d.LastName,
                       Team = d.Team,
                       Country = (tt == null) ? String.Empty : tt.Country
                   };

Inny sposób, z wykorzystaniem domyślnego elementu kolekcji:

F1Team defaultF1Team = new F1Team(String.Empty, String.Empty);

var DriversTeams = from d in DriversChampions
                   join t in F1Teams on d.Team equals t.Name
                   into TempF1Teams
                   from tt in TempF1Teams.DefaultIfEmpty(defaultF1Team)
                   select new
                   {
                       FirstName = d.FirstName,
                       LastName = d.LastName,
                       Team = d.Team,
                       Country = tt.Country
                   };

Zip

Ta metoda nie ma swojego odpowiednika w języku SQL, przy jej użyciu możemy łączyć elementy dwóch kolekcji znajdujące się na tych samych pozycjach (mające ten sam indeks). Jeżeli łączone kolekcje posiadają różną ilość elementów, w wyniku otrzymamy ich tyle ile miała mniejsza z kolekcji. Poniższy przykład prezentuje dwie kolekcje zawierające informacje o czterech okrążeniach pokonanych przez bolid F1. Kolekcja lapTimes przechowuje czasy poszczególnych okrążeń, natomiast kolekcja lapTyres rodzaj opon w jakie wyposażony był bolid. Za pomocą metody Zip informacje te zostały połączone i umieszczone w kolekcji lapInfo. Dodatkowo wykorzystana została przeciążona wersja metody Select pozwalająca na numerowanie poszczególnych elementów kolekcji:

List<TimeSpan> lapTimes = new List<TimeSpan>
{
    new TimeSpan(0, 1, 32, 15, 230),
    new TimeSpan(0, 1, 31, 43, 120),
    new TimeSpan(0, 1, 28, 34, 660),
    new TimeSpan(0, 1, 55, 23, 730)
};

List<string> lapTyres = new List<string>
{
    "Medium",
    "Medium",
    "Soft",
    "Intermediate"
};

var lapInfo = lapTimes.Zip(lapTyres, (time, tyre) =>
    new { LapTime = time, LapTyre = tyre });

var lapInfoFormated = lapInfo.Select((i, n) =>
    string.Format("Okrążenie nr: {0}, Czas: {1}, Opony: {2}", n + 1, i.LapTime, i.LapTyre));

Oto zawartość kolekcji lapInfoFormated:

Okrążenie nr: 1, Czas: 01:32:15.2300000, Opony: Medium
Okrążenie nr: 2, Czas: 01:31:43.1200000, Opony: Medium
Okrążenie nr: 3, Czas: 01:28:34.6600000, Opony: Soft
Okrążenie nr: 4, Czas: 01:55:23.7300000, Opony: Intermediate

Na zakończenie jedna uwaga. Nie ma potrzeby przekazywania obiektu IEqualityComparer<T> do wybranych metod w przypadku łączenia ze sobą kolekcji zawierających referencje do tych samych obiektów.

Reklamy

Posted on 2012-06-28, in .NET/C# and tagged , , , , , , , , . Bookmark the permalink. Dodaj komentarz.

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s

%d blogerów lubi to: