Regroupement des données (C#)

Le regroupement consiste à placer des données dans des groupes afin que les éléments de chaque groupe partagent un attribut commun. L’illustration suivante montre les résultats du regroupement d’une séquence de caractères. La clé de chaque groupe est le caractère.

Diagramme illustrant une opération de regroupement LINQ

Les méthodes d’opérateurs de requête standard qui regroupent les éléments de données sont listées dans le tableau suivant.

Nom de la méthode Description Syntaxe d'expression de requête C# Informations complémentaires
GroupBy Regroupe les éléments qui partagent un attribut commun. Un objet IGrouping<TKey,TElement> représente chaque groupe. group … by

-ou-

group … by … into …
Enumerable.GroupBy

Queryable.GroupBy
ToLookup Insère des éléments dans un Lookup<TKey,TElement> (un dictionnaire de type un-à-plusieurs) basé sur une fonction de sélecteur de clés. Non applicable. Enumerable.ToLookup

L’exemple de code suivant utilise la clause group by pour regrouper des entiers dans une liste selon qu’ils sont pairs ou impairs.

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

La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :

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

Les exemples suivants de cet article utilisent les sources de données courantes pour ce domaine :

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

Chaque Student a un niveau scolaire, un département principal et une série de notes. Un Teacher a également une propriété City qui identifie le campus où l’enseignant donne des cours. Un Department a un nom, et une référence à un Teacher qui est responsable du département.

Regrouper les résultats d’une requête

Le regroupement est l’une des fonctionnalités les plus puissantes de LINQ. Les exemples suivants montrent comment regrouper des données de différentes manières :

  • Selon une propriété
  • Selon la première lettre d’une propriété de chaîne
  • Selon une plage numérique calculée
  • Selon un prédicat booléen ou une autre expression
  • Selon une clé composée

En outre, les deux dernières requêtes projettent leurs résultats dans un nouveau type anonyme qui contient seulement le prénom et le nom de l’étudiant. Pour plus d’informations, consultez group, clause.

Exemple de regroupement par propriété unique

L’exemple suivant montre comment regrouper des éléments source en utilisant une propriété de l’élément comme clé de groupe. La clé est une enum, l’année de l’étudiant dans l’école. L’opération de regroupement utilise le comparateur d’égalité par défaut pour le type.

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

Le code équivalent utilisant la syntaxe de méthode est présenté dans l’exemple suivant :

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

Exemple de groupe par valeur

L’exemple suivant montre comment regrouper des éléments source en utilisant autre chose qu’une propriété de l’objet comme clé de groupe. Dans cet exemple, la clé est la première lettre du nom de famille de l’étudiant.

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

Une boucle foreach imbriquée est nécessaire pour accéder aux éléments des groupes.

Le code équivalent utilisant la syntaxe de méthode est présenté dans l’exemple suivant :

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

Exemple de regroupement par plage

L’exemple suivant montre comment regrouper des éléments source en utilisant une plage numérique comme clé de groupe. La requête projette ensuite les résultats dans un type anonyme qui contient seulement le prénom et le nom de famille de l’étudiant ainsi que la plage du centile où il se trouve. Un type anonyme est utilisé, car il n’est pas nécessaire d’utiliser l’intégralité de l’objet Student pour afficher les résultats. GetPercentile est une fonction d’assistance qui calcule un centile à partir de la note moyenne obtenue par l’étudiant. La méthode retourne un entier compris entre 0 et 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}");
    }
}

Une boucle foreach imbriquée est nécessaire pour itérer dans les groupes et les éléments des groupes. Le code équivalent utilisant la syntaxe de méthode est présenté dans l’exemple suivant :

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

Exemple de regroupement par comparaison

L’exemple suivant montre comment regrouper des éléments source en utilisant une expression de comparaison booléenne. Dans cet exemple, l’expression booléenne teste si la note moyenne d’un étudiant est supérieure à 75. Comme dans les exemples précédents, les résultats sont projetés dans un type anonyme, car l’élément source complet n’est pas nécessaire. Les propriétés du type anonyme deviennent des propriétés sur le membre 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}");
    }
}

La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :

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

Regroupement par type anonyme

L’exemple suivant montre comment utiliser un type anonyme pour encapsuler une clé qui contient plusieurs valeurs. Dans cet exemple, la première valeur de clé est la première lettre du nom de famille de l’étudiant. La deuxième valeur de clé est une valeur booléenne qui spécifie si l’étudiant a obtenu une note supérieure à 85 au premier examen. Vous pouvez organiser les groupes selon n’importe quelle propriété de la clé.

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

La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :

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

Créer un groupe imbriqué

L’exemple suivant montre comment créer des groupes imbriqués dans une expression de requête LINQ. Chaque groupe créé en fonction de l’année/du niveau d’étude est ensuite encore subdivisé en groupes en fonction des noms des individus.

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

Trois boucles foreach imbriquées sont nécessaires pour itérer sur les éléments internes d’un groupe imbriqué.
(Placez le curseur de la souris sur les variables de l’itération, outerGroup, innerGroup et innerGroupElement pour voir leur type réel.)

La requête équivalente utilisant la syntaxe de méthode est illustrée dans le code suivant :

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

Effectuer une sous-requête sur une opération de regroupement

Cet article présente deux façons de créer une requête qui organise les données sources en groupes et effectue ensuite une sous-requête sur chacun de ces groupes. Dans chaque exemple, la technique de base consiste à regrouper les éléments sources en utilisant une continuation nommée newGroup, puis en créant une sous-requête sur newGroup. Cette sous-requête est exécutée sur chaque groupe créé par la requête externe. Dans cet exemple particulier, le résultat final n’est pas un groupe, mais une séquence plate de types anonymes.

Pour plus d’informations sur les regroupements, consultez group, clause. Pour plus d’informations sur les continuations, consultez into. L’exemple suivant utilise une structure de données en mémoire comme source de données, mais les mêmes principes s’appliquent à tous les types de sources de données 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}");
}

La requête de l’extrait de code précédent peut également s’écrire en utilisant la syntaxe de méthode. L’extrait de code suivant comporte une requête sémantiquement équivalente, écrite avec la syntaxe de méthode.

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

Voir aussi