Agrupar datos (C#)
El agrupamiento hace referencia a la operación de colocar los datos en grupos de manera que los elementos de cada grupo compartan un atributo común. La ilustración siguiente muestra los resultados de agrupar una secuencia de caracteres. La clave de cada grupo es el carácter.
Importante
Estos ejemplos usan un origen de datos System.Collections.Generic.IEnumerable<T>. Los orígenes de datos basados en System.Linq.IQueryProvider usanSystem.Linq.IQueryable<T> orígenes de datos y árboles de expresión . Los árboles de expresión tienen limitaciones en la sintaxis de C# permitida. Además, cada origen de datos IQueryProvider
, como EF Core puede imponer más restricciones. Compruebe la documentación del origen de datos.
Los métodos de operador de consulta estándar que agrupan elementos de datos se enumeran en la tabla siguiente.
Nombre del método | Descripción | Sintaxis de la expresión de consulta de C# | Más información |
---|---|---|---|
GroupBy | Agrupa los elementos que comparten un atributo común. Un objeto IGrouping<TKey,TElement> representa cada grupo. | group … by o bien group … by … into … |
Enumerable.GroupBy Queryable.GroupBy |
ToLookup | Inserta elementos a una Lookup<TKey,TElement> (un diccionario uno a varios) basándose en una función de selector de claves. | No aplicable. | Enumerable.ToLookup |
El ejemplo de código siguiente usa la cláusula group by
para agrupar los enteros de una lista según sean pares o impares.
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 consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:
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:
En los ejemplos siguientes de este artículo se usan los orígenes de datos comunes para esta área.
Cada Student
tiene un nivel académico, un departamento principal y una serie de puntuaciones. Un Teacher
también tiene una propiedad City
que identifica el campus donde el profesor imparte clases. Un Department
tiene un nombre y una referencia a un Teacher
que actúa como jefe del departamento.
Puede encontrar el conjunto de datos de ejemplo en el repositorio de origen.
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; }
}
Agrupar los resultados de consultas
La agrupación es una de las capacidades más eficaces de LINQ. Los ejemplos siguientes muestran cómo agrupar datos de varias maneras:
- Por una sola propiedad.
- Por la primera letra de una propiedad de cadena.
- Por un intervalo numérico calculado.
- Por un predicado booleano u otra expresión.
- Por una clave compuesta.
Además, las dos últimas consultas proyectan sus resultados en un nuevo tipo anónimo que solo contiene el nombre y los apellidos del alumno. Para obtener más información, vea la cláusula group.
Ejemplo de agrupación por propiedad única
En el ejemplo siguiente se muestra cómo agrupar elementos de origen mediante una propiedad única del elemento como la clave de grupo. La clave es un elemento enum
, el año del alumno en la escuela. La operación de agrupación usa al comparador de igualdad predeterminado para el 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}");
}
}
El código equivalente mediante la sintaxis del método se muestra en el ejemplo siguiente:
// 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}");
}
}
Ejemplo de agrupación por valor
En el ejemplo siguiente se muestra cómo agrupar elementos de origen mediante algo distinto a una propiedad del objeto para la clave de grupo. En este ejemplo, la clave es la primera letra del apellido del alumno.
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}");
}
}
Se requiere foreach anidado para acceder a los elementos de grupo.
El código equivalente mediante la sintaxis del método se muestra en el ejemplo siguiente:
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}");
}
}
Ejemplo de agrupación por intervalo
En el ejemplo siguiente se muestra cómo agrupar elementos de origen mediante un intervalo numérico como la clave de grupo. Después, la consulta proyecta los resultados en un tipo anónimo que solo contiene el nombre, los apellidos y el intervalo de percentil al que pertenece el alumno. Se usa un tipo anónimo porque no es necesario usar el objeto Student
completo para mostrar los resultados. GetPercentile
es una función del asistente que calcula un percentil basado en la puntuación media del alumno. El método devuelve un entero entre 0 y 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}");
}
}
Se requiere foreach anidado para iterar en grupos y elementos de grupo. El código equivalente mediante la sintaxis del método se muestra en el ejemplo siguiente:
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}");
}
}
Ejemplo de agrupación por comparación
En el ejemplo siguiente se muestra cómo agrupar elementos de origen usando una expresión de comparación booleana. En este ejemplo, la expresión booleana comprueba si la puntuación media del examen de un alumno es mayor de 75. Como en los ejemplos anteriores, los resultados se proyectan en un tipo anónimo porque el elemento de origen completo no es necesario. Las propiedades del tipo anónimo se convierten en propiedades en el miembro 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 consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:
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}");
}
}
Agrupación por tipo anónimo
En el ejemplo siguiente se muestra cómo usar un tipo anónimo para encapsular una clave que contiene varios valores. En este ejemplo, el primer valor de clave es la primera letra del apellido del alumno. El segundo valor de clave es un valor booleano que especifica si el alumno obtuvo una nota superior a 85 en el primer examen. Los grupos se pueden ordenar por cualquier propiedad de la clave.
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 consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:
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}");
}
}
Crear un grupo anidado
En el ejemplo siguiente se muestra cómo crear grupos anidados en una expresión de consulta LINQ. Cada grupo creado a partir del nivel académico o del año de los estudiantes se subdivide en grupos según sus nombres.
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}");
}
}
}
Se necesitan tres bucles foreach
anidados para recorrer en iteración los elementos internos de un grupo anidado.
(Mantenga el cursor del mouse sobre las variables de iteración, outerGroup
, innerGroup
y innerGroupElement
para ver su tipo real).
La consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:
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}");
}
}
}
Realizar una subconsulta en una operación de agrupación
En este artículo se muestran dos maneras diferentes de crear una consulta que ordena los datos de origen en grupos y, luego, realiza una subconsulta en cada grupo de forma individual. La técnica básica de cada ejemplo consiste en agrupar los elementos de origen usando una continuación denominada newGroup
y después generar una nueva subconsulta en newGroup
. Esta subconsulta se ejecuta en cada uno de los nuevos grupos creados por la consulta externa. En este ejemplo concreto el resultado final no es un grupo, sino una secuencia plana de tipos anónimos.
Para obtener más información sobre cómo agrupar, consulte Cláusula group. Para obtener más información sobre continuaciones, consulte into. En el ejemplo siguiente se usa una estructura de datos en memoria como origen de datos, pero se aplican los mismos principios para cualquier tipo de origen de datos 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 consulta del fragmento de código anterior también se puede escribir con la sintaxis de método. El siguiente fragmento de código tiene una consulta semánticamente equivalente escrita con sintaxis 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}");
}