Partilhar via


Agrupamento de dados (C#)

Agrupamento refere-se à operação de colocar dados em grupos para que os elementos em cada grupo compartilhem um atributo comum. A ilustração a seguir mostra os resultados do agrupamento de uma sequência de caracteres. A chave para cada grupo é o personagem.

Diagrama que mostra uma operação de agrupamento LINQ

Importante

Esses exemplos usam uma fonte de System.Collections.Generic.IEnumerable<T> dados. Fontes de dados baseadas em System.Linq.IQueryProvider fontes de dados de uso System.Linq.IQueryable<T> e árvores de expressão. As árvores de expressão têm limitações na sintaxe C# permitida. Além disso, cada IQueryProvider fonte de dados, como o EF Core , pode impor mais restrições. Verifique a documentação da sua fonte de dados.

Os métodos de operador de consulta padrão que agrupam elementos de dados estão listados na tabela a seguir.

Nome do método Description Sintaxe da expressão de consulta C# Mais Informações
GroupBy Agrupa elementos que compartilham um atributo comum. Um IGrouping<TKey,TElement> objeto representa cada grupo. group … by

-or-

group … by … into …
Enumerable.GroupBy

Queryable.GroupBy
ToLookup Insere elementos em um Lookup<TKey,TElement> (um dicionário um-para-muitos) com base em uma função seletora de teclas. Não aplicável. Enumerable.ToLookup

O exemplo de código a seguir usa a group by cláusula para agrupar inteiros em uma lista de acordo com se eles são pares ou ímpares.

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 consulta equivalente usando sintaxe de método é mostrada no código a seguir:

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

Nota

Os exemplos a seguir neste artigo usam as fontes de dados comuns para essa área.
Cada Student um tem um nível de nota, um departamento primário e uma série de pontuações. A Teacher também tem uma City propriedade que identifica o campus onde o professor tem aulas. A Department tem um nome, e uma referência a um Teacher que serve como chefe de departamento.
Você pode encontrar o conjunto de dados de exemplo no repositório de origem.

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

Resultados da consulta de grupo

O agrupamento é um dos recursos mais poderosos do LINQ. Os exemplos a seguir mostram como agrupar dados de várias maneiras:

  • Por uma única propriedade.
  • Pela primeira letra de uma propriedade string.
  • Por um intervalo numérico calculado.
  • Por predicado booleano ou outra expressão.
  • Por uma chave composta.

Além disso, as duas últimas consultas projetam seus resultados em um novo tipo anônimo que contém apenas o nome do aluno e o nome da família. Para obter mais informações, consulte a cláusula de grupo.

Exemplo de agrupamento por propriedade única

O exemplo a seguir mostra como agrupar elementos de origem usando uma única propriedade do elemento como a chave de grupo. A chave é um enum, o ano do aluno na escola. A operação de agrupamento usa o comparador de igualdade padrão para o tipo.

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

O código equivalente usando a sintaxe do método é mostrado no exemplo a seguir:

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

Exemplo de agrupamento por valor

O exemplo a seguir mostra como agrupar elementos de origem usando algo diferente de uma propriedade do objeto para a chave de grupo. Neste exemplo, a chave é a primeira letra do nome de família do aluno.

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

Foreach aninhado é necessário para acessar itens de grupo.

O código equivalente usando a sintaxe do método é mostrado no exemplo a seguir:

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

Agrupar por um exemplo de intervalo

O exemplo a seguir mostra como agrupar elementos de origem usando um intervalo numérico como uma chave de grupo. Em seguida, a consulta projeta os resultados em um tipo anônimo que contém apenas o primeiro nome e o nome da família e o intervalo de percentis ao qual o aluno pertence. Um tipo anônimo é usado porque não é necessário usar o objeto completo Student para exibir os resultados. GetPercentile é uma função auxiliar que calcula um percentil com base na pontuação média do aluno. O método retorna um inteiro entre 0 e 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}");
    }
}

Foreach aninhado necessário para iterar sobre grupos e itens de grupo. O código equivalente usando a sintaxe do método é mostrado no exemplo a seguir:

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

Exemplo de agrupamento por comparação

O exemplo a seguir mostra como agrupar elementos de origem usando uma expressão de comparação booleana. Neste exemplo, a expressão booleana testa se a pontuação média de um aluno no exame é maior que 75. Como nos exemplos anteriores, os resultados são projetados em um tipo anônimo porque o elemento de origem completo não é necessário. As propriedades no tipo anônimo tornam-se propriedades no Key membro.

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 consulta equivalente usando sintaxe de método é mostrada no código a seguir:

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

Agrupar por tipo anónimo

O exemplo a seguir mostra como usar um tipo anônimo para encapsular uma chave que contém vários valores. Neste exemplo, o primeiro valor de chave é a primeira letra do nome de família do aluno. O segundo valor-chave é um booleano que especifica se o aluno obteve mais de 85 pontos no primeiro exame. Você pode ordenar os grupos por qualquer propriedade na chave.

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 consulta equivalente usando sintaxe de método é mostrada no código a seguir:

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

Criar um grupo aninhado

O exemplo a seguir mostra como criar grupos aninhados em uma expressão de consulta LINQ. Cada grupo que é criado de acordo com o ano do aluno ou nível de escolaridade é então subdividido em grupos com base nos nomes dos indivíduos.

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

Três loops aninhados foreach são necessários para iterar sobre os elementos internos de um grupo aninhado.
(Passe o cursor do mouse sobre as variáveis de iteração, outerGroup, innerGroupe innerGroupElement para ver seu tipo real.)

A consulta equivalente usando sintaxe de método é mostrada no código a seguir:

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

Executar uma subconsulta em uma operação de agrupamento

Este artigo mostra duas maneiras diferentes de criar uma consulta que ordena os dados de origem em grupos e, em seguida, executa uma subconsulta sobre cada grupo individualmente. A técnica básica em cada exemplo é agrupar os elementos de origem usando uma continuação chamada newGroupe, em seguida, gerar uma nova subconsulta em relação a newGroup. Esta subconsulta é executada em cada novo grupo criado pela consulta externa. Neste exemplo em particular, a saída final não é um grupo, mas uma sequência plana de tipos anônimos.

Para obter mais informações sobre como agrupar, consulte cláusula de grupo. Para obter mais informações sobre continuações, consulte into. O exemplo a seguir usa uma estrutura de dados na memória como fonte de dados, mas os mesmos princípios se aplicam a qualquer tipo de fonte de dados 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}");
}

A consulta no trecho anterior também pode ser escrita usando a sintaxe do método. O trecho de código a seguir tem uma consulta semanticamente equivalente escrita usando sintaxe de método.

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

Consulte também