Seskupování dat (C#)

Seskupení odkazuje na operaci vkládání dat do skupin tak, aby prvky v každé skupině sdílely společný atribut. Následující obrázek ukazuje výsledky seskupení posloupnosti znaků. Klíč pro každou skupinu je znak.

Diagram znázorňující operaci seskupování LINQ

Standardní metody operátoru dotazu, které seskupují datové prvky, jsou uvedeny v následující tabulce.

Název metody Popis Syntaxe výrazu dotazu jazyka C# Další informace
GroupBy Seskupuje prvky, které sdílejí společný atribut. Objekt IGrouping<TKey,TElement> představuje každou skupinu. group … by

nebo

group … by … into …
Enumerable.GroupBy

Queryable.GroupBy
ToLookup Vloží prvky do slovníku Lookup<TKey,TElement> 1:N na základě funkce selektoru klíče. Nevztahuje se. Enumerable.ToLookup

Následující příklad kódu používá group by klauzuli k seskupení celých čísel v seznamu podle toho, zda jsou sudé nebo liché.

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);
    }
}

Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:

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);
    }
}

Následující příklady v tomto článku používají běžné zdroje dat pro tuto oblast:

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ždý z nich Student má úroveň známek, primární oddělení a řadu výsledků. A TeacherCity také vlastnost, která identifikuje areál, kde učitel má předměty. A Department má jméno a odkaz na Teacher toho, kdo slouží jako vedoucí oddělení.

Seskupení výsledků dotazu

Seskupování je jednou z nejvýkonnějších funkcí LINQ. Následující příklady ukazují, jak seskupit data různými způsoby:

  • Jednou vlastností.
  • Podle prvního písmena vlastnosti řetězce.
  • Vypočítaným číselným rozsahem.
  • Logickým predikátem nebo jiným výrazem.
  • Složeným klíčem.

Kromě toho poslední dva dotazy promítnou výsledky do nového anonymního typu, který obsahuje pouze jméno studenta a jméno rodiny. Další informace najdete v klauzuli group.

Příklad seskupení podle jedné vlastnosti

Následující příklad ukazuje, jak seskupit zdrojové elementy pomocí jedné vlastnosti elementu jako klíče skupiny. Klíčem je enumrok studenta ve škole. Operace seskupení používá výchozí porovnávač rovnosti pro typ.

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}");
    }
}

Ekvivalentní kód používající syntaxi metody je znázorněn v následujícím příkladu:

// 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}");
    }
}

Příklad seskupení podle hodnoty

Následující příklad ukazuje, jak seskupit zdrojové elementy pomocí něčeho jiného než vlastnosti objektu pro klíč skupiny. V tomto příkladu je klíčem první písmeno jména studenta.

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}");
    }
}

Pro přístup k položkám skupiny se vyžaduje vnořený foreach.

Ekvivalentní kód používající syntaxi metody je znázorněn v následujícím příkladu:

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}");
    }
}

Seskupení podle příkladu rozsahu

Následující příklad ukazuje, jak seskupit zdrojové prvky pomocí číselného rozsahu jako klíče skupiny. Dotaz pak výsledky prodá do anonymního typu, který obsahuje pouze jméno a jméno rodiny a rozsah percentilu, do kterého student patří. Používá se anonymní typ, protože k zobrazení výsledků není nutné použít úplný Student objekt. GetPercentile je pomocná funkce, která vypočítá percentil na základě průměrného skóre studenta. Metoda vrátí celé číslo mezi 0 a 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}");
    }
}

Vnořená foreach potřebná k iteraci nad skupinami a položkami skupiny Ekvivalentní kód používající syntaxi metody je znázorněn v následujícím příkladu:

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}");
    }
}

Příklad seskupení podle porovnání

Následující příklad ukazuje, jak seskupit zdrojové elementy pomocí logického porovnávací výrazu. V tomto příkladu logický výraz testuje, jestli je průměrné skóre zkoušky studenta větší než 75. Stejně jako v předchozích příkladech se výsledky promítají do anonymního typu, protože úplný zdrojový prvek není potřeba. Vlastnosti v anonymním typu se stanou vlastnostmi člena Key .

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}");
    }
}

Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:

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}");
    }
}

Seskupovat podle anonymního typu

Následující příklad ukazuje, jak použít anonymní typ k zapouzdření klíče, který obsahuje více hodnot. V tomto příkladu je první hodnota klíče prvním písmenem jména studenta. Druhá hodnota klíče je logická hodnota, která určuje, jestli student získal skóre více než 85 při první zkoušce. Skupiny můžete uspořádat podle libovolné vlastnosti v klíči.

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}");
    }
}

Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:

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}");
    }
}

Vytvoření vnořené skupiny

Následující příklad ukazuje, jak vytvořit vnořené skupiny ve výrazu dotazu LINQ. Každá skupina vytvořená podle ročníku studenta nebo úrovně známky se pak dále rozdělí do skupin založených na jménech jednotlivců.

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}");
        }
    }
}

K iteraci vnitřních prvků vnořené skupiny se vyžadují tři vnořené foreach smyčky.
(Najeďte myší na proměnné iterace, outerGroup, innerGroupa innerGroupElement zobrazte jejich skutečný typ.)

Ekvivalentní dotaz pomocí syntaxe metody se zobrazí v následujícím kódu:

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}");
        }
    }
}

Provádění poddotazů na skupinách

Tento článek ukazuje dva různé způsoby vytvoření dotazu, který seřazuje zdrojová data do skupin, a potom provede poddotaz pro každou skupinu jednotlivě. Základní technika v každém příkladu je seskupit zdrojové prvky pomocí pokračování s názvem newGroup, a pak vygenerovat nový poddotaz proti newGroup . Tento poddotaz se spustí pro každou novou skupinu vytvořenou vnějším dotazem. V tomto konkrétním příkladu není konečný výstup skupinou, ale plochou sekvencí anonymních typů.

Další informace o seskupování najdete v klauzuli group. Další informace o pokračováních najdete v tématu. Následující příklad používá datovou strukturu v paměti jako zdroj dat, ale stejné principy platí pro jakýkoli druh zdroje dat 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}");
}

Dotaz v předchozím fragmentu kódu lze také zapsat pomocí syntaxe metody. Následující fragment kódu obsahuje sémanticky ekvivalentní dotaz napsaný pomocí syntaxe 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}");
}

Viz také