Adatok csoportosítása (C#)

A csoportosítás az adatok csoportokba helyezésének műveletére utal, így az egyes csoportok elemei közös attribútumban osztoznak. Az alábbi ábrán egy karaktersorozat csoportosításának eredményei láthatók. Az egyes csoportok kulcsa a karakter.

LINQ-csoportosítási műveletet bemutató diagram

Az adatelemeket csoportosító szabványos lekérdezésoperátor-metódusok az alábbi táblázatban találhatók.

Metódus neve Leírás C# lekérdezési kifejezés szintaxisa További információ
GroupBy Csoportosítja azokat az elemeket, amelyek közös attribútummal rendelkeznek. Az IGrouping<TKey,TElement> objektumok az egyes csoportokat jelölik. group … by

-vagy-

group … by … into …
Enumerable.GroupBy

Queryable.GroupBy
ToLookup Elemek beszúrása egy Lookup<TKey,TElement> (egy-a-többhöz) szótárba egy kulcsválasztó függvény alapján. Nem alkalmazható. Enumerable.ToLookup

Az alábbi példakód a group by záradék használatával csoportosítja az egész számokat egy listában attól függően, hogy párosak vagy páratlanok-e.

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

A metódusszintaxissal egyenértékű lekérdezés a következő kódban jelenik meg:

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

A cikkben szereplő alábbi példák a terület közös adatforrásait használják:

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

Mindegyiknek Student van egy osztályszintje, egy elsődleges osztálya és egy sor pontszáma. Az A-nek Teacher is van egy City tulajdonsága, amely azonosítja azt a campust, ahol a tanár órákat tart. Az A-nek Department van egy neve, és egy olyan személyre Teacher való hivatkozás, aki az osztályvezető.

Lekérdezési eredmények csoportosítása

A csoportosítás a LINQ egyik leghatékonyabb képessége. Az alábbi példák bemutatják, hogyan csoportosíthatja az adatokat különböző módokon:

  • Egyetlen tulajdonság alapján.
  • Egy sztringtulajdonság első betűje szerint.
  • Számított numerikus tartomány szerint.
  • Logikai predikátum vagy más kifejezés szerint.
  • Összetett kulccsal.

Emellett az utolsó két lekérdezés az eredményeket egy új névtelen típusba veti, amely csak a tanuló vezeték- és családnevét tartalmazza. További információt a csoport záradékában talál.

Csoportosítás egyetlen tulajdonság alapján – példa

Az alábbi példa bemutatja, hogyan csoportosíthatja a forráselemeket úgy, hogy az elem egyetlen tulajdonságát használja csoportkulcsként. A kulcs egy enum, a tanulói év az iskolában. A csoportosítási művelet a típus alapértelmezett egyenlőség-összehasonlítóját használja.

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

A metódusszintaxissal egyenértékű kód az alábbi példában látható:

// 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élda érték szerinti csoportosításra

Az alábbi példa bemutatja, hogyan csoportosíthatja a forráselemeket a csoportkulcs objektumának tulajdonságától eltérő tulajdonság használatával. Ebben a példában a kulcs a tanuló családi nevének első betűje.

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

A csoportelemek eléréséhez beágyazott foreach szükséges.

A metódusszintaxissal egyenértékű kód az alábbi példában látható:

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

Csoportosítás tartomány szerint – példa

Az alábbi példa bemutatja, hogyan csoportosíthatja a forráselemeket numerikus tartomány használatával csoportkulcsként. A lekérdezés ezután névtelen típusba alakítja az eredményeket, amely csak az utó- és családnevet, valamint azt a percentilistartományt tartalmazza, amelyhez a tanuló tartozik. A rendszer névtelen típust használ, mert nem szükséges a teljes Student objektumot használni az eredmények megjelenítéséhez. GetPercentile egy segédfüggvény, amely a tanuló átlagos pontszáma alapján számít ki egy percentiliset. A metódus 0 és 10 közötti egész számot ad vissza.

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

A csoportok és csoportelemek iterálásához beágyazott foreach szükséges. A metódusszintaxissal egyenértékű kód az alábbi példában látható:

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

Csoportosítás összehasonlítási példa alapján

Az alábbi példa bemutatja, hogyan csoportosíthatja a forráselemeket logikai összehasonlító kifejezéssel. Ebben a példában a logikai kifejezés azt vizsgálja, hogy egy tanuló átlagos vizsgaeredménye nagyobb-e, mint 75. A korábbi példákhoz hasonlóan az eredmények névtelen típusba lesznek vetítve, mivel a teljes forráselemre nincs szükség. A névtelen típusú tulajdonságok tulajdonságokká válnak a Key tagon.

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

A metódusszintaxissal egyenértékű lekérdezés a következő kódban jelenik meg:

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

Csoportosítás névtelen típus szerint

Az alábbi példa bemutatja, hogyan használhat névtelen típust egy több értéket tartalmazó kulcs beágyazásához. Ebben a példában az első kulcsérték a tanuló családi nevének első betűje. A második kulcsérték egy logikai érték, amely meghatározza, hogy a tanuló több mint 85 pontot ért-e el az első vizsgán. A csoportokat a kulcs bármely tulajdonsága alapján rendezheti.

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

A metódusszintaxissal egyenértékű lekérdezés a következő kódban jelenik meg:

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

Beágyazott csoport létrehozása

Az alábbi példa bemutatja, hogyan hozhat létre beágyazott csoportokat egy LINQ-lekérdezési kifejezésben. A tanulói év vagy osztályzat szerint létrehozott csoportokat ezután tovább osztják csoportokra az egyének neve alapján.

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

A beágyazott csoportok belső elemein való iteráláshoz három beágyazott foreach hurok szükséges.
(Vigye az egérmutatót az iterációs változók fölé, outerGroupinnerGroupés innerGroupElement tekintse meg a tényleges típusukat.)

A metódusszintaxissal egyenértékű lekérdezés a következő kódban jelenik meg:

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

Al lekérdezés végrehajtása csoportosítási műveleten

Ez a cikk két különböző módszert mutat be egy olyan lekérdezés létrehozására, amely csoportokba rendezi a forrásadatokat, majd egyenként hajt végre al lekérdezéseket az egyes csoportokon. Az egyes példák alapvető technikája a forráselemek csoportosítása egy nevesített newGroupfolytatással, majd egy új alquery létrehozása.newGroup Ezt az al lekérdezést a külső lekérdezés által létrehozott új csoportokon futtatja a rendszer. Ebben a példában a végső kimenet nem csoport, hanem névtelen típusok egybesimított sorozata.

A csoportosításról további információt a csoport záradékában talál. További információ a folytatásokról: Az alábbi példa egy memórián belüli adatstruktúrát használ adatforrásként, de ugyanezek az alapelvek vonatkoznak bármilyen LINQ-adatforrásra.

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

Az előző kódrészlet lekérdezése metódusszintaxissal is írható. A következő kódrészlet egy szemantikailag egyenértékű lekérdezéssel rendelkezik, amely metódusszintaxissal íródott.

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

Lásd még