Grupowanie danych (C#)

Grupowanie odnosi się do operacji umieszczania danych w grupach, aby elementy w każdej grupie współdzieliły wspólny atrybut. Poniższa ilustracja przedstawia wyniki grupowania sekwencji znaków. Kluczem dla każdej grupy jest znak.

Diagram przedstawiający operację grupowania LINQ

Standardowe metody operatorów zapytań, które grupują elementy danych, są wymienione w poniższej tabeli.

Nazwa metody opis Składnia wyrażeń zapytań języka C# Więcej informacji
GroupBy Grupy elementów, które mają wspólny atrybut. Obiekt IGrouping<TKey,TElement> reprezentuje każdą grupę. group … by

— lub —

group … by … into …
Enumerable.GroupBy

Queryable.GroupBy
Tolookup Wstawia elementy do słownika Lookup<TKey,TElement> (jeden do wielu) na podstawie funkcji selektora kluczy. Nie dotyczy. Enumerable.ToLookup

Poniższy przykład kodu używa klauzuli group by do grupowania liczb całkowitych na liście zgodnie z tym, czy są parzyste, czy dziwne.

List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];

IEnumerable<IGrouping<int, int>> query = from number in numbers
                                         group number by number % 2;

foreach (var group in query)
{
    Console.WriteLine(group.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
    foreach (int i in group)
    {
        Console.WriteLine(i);
    }
}

Równoważne zapytanie używające składni metody jest wyświetlane w następującym kodzie:

List<int> numbers = [35, 44, 200, 84, 3987, 4, 199, 329, 446, 208];

IEnumerable<IGrouping<int, int>> query = numbers
    .GroupBy(number => number % 2);

foreach (var group in query)
{
    Console.WriteLine(group.Key == 0 ? "\nEven numbers:" : "\nOdd numbers:");
    foreach (int i in group)
    {
        Console.WriteLine(i);
    }
}

W poniższych przykładach w tym artykule użyto typowych źródeł danych dla tego obszaru:

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required int ID { get; init; }

    public required GradeLevel Year { get; init; }
    public required List<int> Scores { get; init; }

    public required int DepartmentID { get; init; }
}

public class Teacher
{
    public required string First { get; init; }
    public required string Last { get; init; }
    public required int ID { get; init; }
    public required string City { get; init; }
}
public class Department
{
    public required string Name { get; init; }
    public int ID { get; init; }

    public required int TeacherID { get; init; }
}

Każdy z nich Student ma poziom klasy, dział podstawowy i serię wyników. Obiekt Teacher ma również właściwość identyfikującą City kampus, w którym nauczyciel posiada zajęcia. Element Department ma nazwę i odwołanie do osoby Teacher , która służy jako szef działu.

Grupowanie wyników zapytania

Grupowanie jest jedną z najbardziej zaawansowanych funkcji LINQ. W poniższych przykładach pokazano, jak grupować dane na różne sposoby:

  • Według jednej właściwości.
  • Przez pierwszą literę właściwości ciągu.
  • Według obliczonego zakresu liczbowego.
  • Według predykatu logicznego lub innego wyrażenia.
  • Za pomocą klucza złożonego.

Ponadto dwa ostatnie zapytania projektują wyniki w nowy typ anonimowy, który zawiera tylko imię i nazwisko ucznia. Aby uzyskać więcej informacji, zobacz klauzulę group.

Przykład grupowania według pojedynczej właściwości

W poniższym przykładzie pokazano, jak grupować elementy źródłowe przy użyciu pojedynczej właściwości elementu jako klucza grupy. Kluczem jest enum, rok ucznia w szkole. Operacja grupowania używa domyślnego porównania równości dla typu.

var groupByYearQuery =
    from student in students
    group student by student.Year into newGroup
    orderby newGroup.Key
    select newGroup;

foreach (var yearGroup in groupByYearQuery)
{
    Console.WriteLine($"Key: {yearGroup.Key}");
    foreach (var student in yearGroup)
    {
        Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
    }
}

Równoważny kod używający składni metody jest pokazany w poniższym przykładzie:

// Variable groupByLastNamesQuery is an IEnumerable<IGrouping<string,
// DataClass.Student>>.
var groupByYearQuery = students
    .GroupBy(student => student.Year)
    .OrderBy(newGroup => newGroup.Key);

foreach (var yearGroup in groupByYearQuery)
{
    Console.WriteLine($"Key: {yearGroup.Key}");
    foreach (var student in yearGroup)
    {
        Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
    }
}

Przykład grupowania według wartości

W poniższym przykładzie pokazano, jak grupować elementy źródłowe przy użyciu innej właściwości niż właściwość obiektu dla klucza grupy. W tym przykładzie kluczem jest pierwsza litera imienia i nazwiska ucznia.

var groupByFirstLetterQuery =
    from student in students
    let firstLetter = student.LastName[0]
    group student by firstLetter;

foreach (var studentGroup in groupByFirstLetterQuery)
{
    Console.WriteLine($"Key: {studentGroup.Key}");
    foreach (var student in studentGroup)
    {
        Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
    }
}

Aby uzyskać dostęp do elementów grupy, wymagany jest zagnieżdżony foreach.

Równoważny kod używający składni metody jest pokazany w poniższym przykładzie:

var groupByFirstLetterQuery = students
    .GroupBy(student => student.LastName[0]);

foreach (var studentGroup in groupByFirstLetterQuery)
{
    Console.WriteLine($"Key: {studentGroup.Key}");
    foreach (var student in studentGroup)
    {
        Console.WriteLine($"\t{student.LastName}, {student.FirstName}");
    }
}

Grupuj według przykładu zakresu

W poniższym przykładzie pokazano, jak grupować elementy źródłowe przy użyciu zakresu liczbowego jako klucza grupy. Następnie zapytanie wysyła wyniki do typu anonimowego, który zawiera tylko imię i nazwisko rodziny oraz zakres percentylu, do którego należy student. Używany jest typ anonimowy, ponieważ nie jest konieczne użycie kompletnego Student obiektu w celu wyświetlenia wyników. GetPercentile jest funkcją pomocnika, która oblicza percentyl na podstawie średniego wyniku ucznia. Metoda zwraca liczbę całkowitą z zakresu od 0 do 10.

static int GetPercentile(Student s)
{
    double avg = s.Scores.Average();
    return avg > 0 ? (int)avg / 10 : 0;
}

var groupByPercentileQuery =
    from student in students
    let percentile = GetPercentile(student)
    group new
    {
        student.FirstName,
        student.LastName
    } by percentile into percentGroup
    orderby percentGroup.Key
    select percentGroup;

foreach (var studentGroup in groupByPercentileQuery)
{
    Console.WriteLine($"Key: {studentGroup.Key * 10}");
    foreach (var item in studentGroup)
    {
        Console.WriteLine($"\t{item.LastName}, {item.FirstName}");
    }
}

Zagnieżdżone foreach wymagane do iteracji grup i elementów grupy. Równoważny kod używający składni metody jest pokazany w poniższym przykładzie:

static int GetPercentile(Student s)
{
    double avg = s.Scores.Average();
    return avg > 0 ? (int)avg / 10 : 0;
}

var groupByPercentileQuery = students
    .Select(student => new { student, percentile = GetPercentile(student) })
    .GroupBy(student => student.percentile)
    .Select(percentGroup => new
    {
        percentGroup.Key,
        Students = percentGroup.Select(s => new { s.student.FirstName, s.student.LastName })
    })
    .OrderBy(percentGroup => percentGroup.Key);

foreach (var studentGroup in groupByPercentileQuery)
{
    Console.WriteLine($"Key: {studentGroup.Key * 10}");
    foreach (var item in studentGroup.Students)
    {
        Console.WriteLine($"\t{item.LastName}, {item.FirstName}");
    }
}

Przykład grupowania według porównania

W poniższym przykładzie pokazano, jak grupować elementy źródłowe przy użyciu wyrażenia porównania warunkowego. W tym przykładzie wyrażenie logiczne sprawdza, czy średni wynik egzaminu ucznia jest większy niż 75. Podobnie jak w poprzednich przykładach, wyniki są projektowane w typ anonimowy, ponieważ kompletny element źródłowy nie jest potrzebny. Właściwości w typie anonimowym stają się właściwościami elementu Key członkowskiego.

var groupByHighAverageQuery =
    from student in students
    group new
    {
        student.FirstName,
        student.LastName
    } by student.Scores.Average() > 75 into studentGroup
    select studentGroup;

foreach (var studentGroup in groupByHighAverageQuery)
{
    Console.WriteLine($"Key: {studentGroup.Key}");
    foreach (var student in studentGroup)
    {
        Console.WriteLine($"\t{student.FirstName} {student.LastName}");
    }
}

Równoważne zapytanie używające składni metody jest wyświetlane w następującym kodzie:

var groupByHighAverageQuery = students
    .GroupBy(student => student.Scores.Average() > 75)
    .Select(group => new
    {
        group.Key,
        Students = group.AsEnumerable().Select(s => new { s.FirstName, s.LastName })
    });

foreach (var studentGroup in groupByHighAverageQuery)
{
    Console.WriteLine($"Key: {studentGroup.Key}");
    foreach (var student in studentGroup.Students)
    {
        Console.WriteLine($"\t{student.FirstName} {student.LastName}");
    }
}

Grupuj według typu anonimowego

W poniższym przykładzie pokazano, jak używać typu anonimowego do hermetyzacji klucza zawierającego wiele wartości. W tym przykładzie pierwsza wartość klucza to pierwsza litera nazwy rodziny ucznia. Druga wartość klucza to wartość logiczna określająca, czy student zdobył ponad 85 na pierwszym egzaminie. Grupy można porządkować według dowolnej właściwości w kluczu.

var groupByCompoundKey =
    from student in students
    group student by new
    {
        FirstLetterOfLastName = student.LastName[0],
        IsScoreOver85 = student.Scores[0] > 85
    } into studentGroup
    orderby studentGroup.Key.FirstLetterOfLastName
    select studentGroup;

foreach (var scoreGroup in groupByCompoundKey)
{
    var s = scoreGroup.Key.IsScoreOver85 ? "more than 85" : "less than 85";
    Console.WriteLine($"Name starts with {scoreGroup.Key.FirstLetterOfLastName} who scored {s}");
    foreach (var item in scoreGroup)
    {
        Console.WriteLine($"\t{item.FirstName} {item.LastName}");
    }
}

Równoważne zapytanie używające składni metody jest wyświetlane w następującym kodzie:

var groupByCompoundKey = students
    .GroupBy(student => new
    {
        FirstLetterOfLastName = student.LastName[0],
        IsScoreOver85 = student.Scores[0] > 85
    })
    .OrderBy(studentGroup => studentGroup.Key.FirstLetterOfLastName);

foreach (var scoreGroup in groupByCompoundKey)
{
    var s = scoreGroup.Key.IsScoreOver85 ? "more than 85" : "less than 85";
    Console.WriteLine($"Name starts with {scoreGroup.Key.FirstLetterOfLastName} who scored {s}");
    foreach (var item in scoreGroup)
    {
        Console.WriteLine($"\t{item.FirstName} {item.LastName}");
    }
}

Tworzenie grupy zagnieżdżonej

W poniższym przykładzie pokazano, jak utworzyć grupy zagnieżdżone w wyrażeniu zapytania LINQ. Każda grupa, która jest tworzona zgodnie z poziomem roku lub klasy studenta, jest następnie dalej podzielona na grupy na podstawie nazwisk osób.

var nestedGroupsQuery =
    from student in students
    group student by student.Year into newGroup1
    from newGroup2 in
    from student in newGroup1
    group student by student.LastName
    group newGroup2 by newGroup1.Key;

foreach (var outerGroup in nestedGroupsQuery)
{
    Console.WriteLine($"DataClass.Student Level = {outerGroup.Key}");
    foreach (var innerGroup in outerGroup)
    {
        Console.WriteLine($"\tNames that begin with: {innerGroup.Key}");
        foreach (var innerGroupElement in innerGroup)
        {
            Console.WriteLine($"\t\t{innerGroupElement.LastName} {innerGroupElement.FirstName}");
        }
    }
}

Do iterowania elementów wewnętrznych grupy zagnieżdżonej wymagane są trzy zagnieżdżone foreach pętle.
(Umieść kursor myszy na zmiennych iteracji, outerGroup, innerGroupi, innerGroupElement aby wyświetlić ich rzeczywisty typ).

Równoważne zapytanie używające składni metody jest wyświetlane w następującym kodzie:

var nestedGroupsQuery =
    students
    .GroupBy(student => student.Year)
    .Select(newGroup1 => new
    {
        newGroup1.Key,
        NestedGroup = newGroup1
            .GroupBy(student => student.LastName)
    });

foreach (var outerGroup in nestedGroupsQuery)
{
    Console.WriteLine($"DataClass.Student Level = {outerGroup.Key}");
    foreach (var innerGroup in outerGroup.NestedGroup)
    {
        Console.WriteLine($"\tNames that begin with: {innerGroup.Key}");
        foreach (var innerGroupElement in innerGroup)
        {
            Console.WriteLine($"\t\t{innerGroupElement.LastName} {innerGroupElement.FirstName}");
        }
    }
}

Wykonywanie podzapytania w operacji grupowania

W tym artykule przedstawiono dwa różne sposoby tworzenia zapytania, które porządkuje dane źródłowe w grupach, a następnie wykonuje podzapytywanie poszczególnych grup osobno. Podstawową techniką w każdym przykładzie jest grupowanie elementów źródłowych przy użyciu kontynuacji o nazwie newGroup, a następnie generowanie nowego podzapytania względem newGroupelementu . To podzapytywanie jest uruchamiane względem każdej nowej grupy utworzonej przez zapytanie zewnętrzne. W tym konkretnym przykładzie ostateczne dane wyjściowe nie są grupą, ale płaską sekwencją typów anonimowych.

Aby uzyskać więcej informacji na temat sposobu grupowania, zobacz klauzulę group. Aby uzyskać więcej informacji na temat kontynuacji, zobacz. W poniższym przykładzie użyto struktury danych w pamięci jako źródła danych, ale te same zasady dotyczą dowolnego rodzaju źródła danych LINQ.

var queryGroupMax =
    from student in students
    group student by student.Year into studentGroup
    select new
    {
        Level = studentGroup.Key,
        HighestScore = (
            from student2 in studentGroup
            select student2.Scores.Average()
        ).Max()
    };

var count = queryGroupMax.Count();
Console.WriteLine($"Number of groups = {count}");

foreach (var item in queryGroupMax)
{
    Console.WriteLine($"  {item.Level} Highest Score={item.HighestScore}");
}

Zapytanie w poprzednim fragmencie kodu można również napisać przy użyciu składni metody. Poniższy fragment kodu ma semantycznie równoważne zapytanie napisane przy użyciu składni metody.

var queryGroupMax =
    students
        .GroupBy(student => student.Year)
        .Select(studentGroup => new
        {
            Level = studentGroup.Key,
            HighestScore = studentGroup.Max(student2 => student2.Scores.Average())
        });

var count = queryGroupMax.Count();
Console.WriteLine($"Number of groups = {count}");

foreach (var item in queryGroupMax)
{
    Console.WriteLine($"  {item.Level} Highest Score={item.HighestScore}");
}

Zobacz też