1. Introducción
Cuando trabajamos con colecciones, muchas veces no solo queremos recorrer cada elemento y hacer algo, sino obtener un pequeño "resumen" de todo el conjunto de datos. Por ejemplo:
- Contar cuántos estudiantes sacaron un cinco.
- Saber cuántos alumnos hay en el cole.
- Sumar cuántos puntos sacaron todos los estudiantes en el examen.
- Encontrar la nota máxima y mínima.
- Calcular la media de la clase.
Claro, todas estas tareas se pueden resolver con un bucle normal y una variable contador. Pero seamos sinceros: a nadie le apetece escribir veinte líneas de código para algo tan básico.
LINQ nos da un set de funciones agregadas — métodos ya hechos que cogen la colección, la recorren y te dan la respuesta: suma, media, máximo, mínimo, y a veces cosas más rebuscadas.
Aquí tienes una "tabla de agregados" rápida:
| Método | Qué hace | Devuelve |
|---|---|---|
|
Cuenta el número de elementos | |
|
Suma los valores numéricos | Depende del tipo de los elementos (, , ...) |
|
Calcula la media aritmética | u otro tipo numérico |
|
Busca el elemento máximo | Elemento de la colección |
|
Busca el elemento mínimo | Elemento de la colección |
Todos estos métodos son extension-methods para colecciones que implementan IEnumerable<T>. Más info — documentación oficial de métodos agregados LINQ.
2. Preparamos los datos para practicar
Vamos a seguir con una app sencilla para gestionar estudiantes. Imagina que tenemos esta clase:
// Modelo de estudiante
public class Student
{
public string Name { get; set; }
public int Grade { get; set; } // Nota en escala de cinco
public string Email { get; set; }
}
Y una lista:
// Lista básica de estudiantes
List<Student> students = new List<Student>
{
new Student { Name = "Iván", Grade = 5, Email = "ivan@example.com" },
new Student { Name = "Olga", Grade = 4, Email = "olga@example.com" },
new Student { Name = "Artem", Grade = 3, Email = "artem@example.com" },
new Student { Name = "Daria", Grade = 5, Email = "darya@example.com" },
new Student { Name = "Petr", Grade = 2, Email = "petr@example.com" }
};
3. Contar elementos: Count()
Estadística básica
Por ejemplo, quieres saber cuántos estudiantes tienes:
int totalStudents = students.Count(); // 5
Console.WriteLine($"Total de estudiantes: {totalStudents}");
Magia LINQ: ¡Así de fácil! El método Count() te devuelve el número de elementos en la colección.
Contar con condición
¿Y cuántos son sobresalientes?
int excellentStudents = students.Count(s => s.Grade == 5);
Console.WriteLine($"Sobresalientes: {excellentStudents}");
Aquí le pasamos a Count una lambda — solo cuenta los que cumplen la condición (s.Grade == 5). Por dentro, en LINQ es como Where(...).Count(), pero más corto y un pelín más eficiente.
¿Y si la colección está vacía?
Si la colección está vacía, Count devuelve 0 — no hay error ni nada raro.
4. Sumar valores: Sum()
Obteniendo la suma de todas las notas
Imagina que quieres saber la suma total de puntos que ha sacado la clase:
int sumOfGrades = students.Sum(s => s.Grade); // 5+4+3+5+2 = 19
Console.WriteLine($"Suma de todas las notas: {sumOfGrades}");
Sum recibe un selector (una lambda) que devuelve el valor de cada elemento.
Sumando en una colección de números
Si tienes una lista de números, no necesitas selector:
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Sum(); // 15
Errores comunes y matices
Si la colección está vacía, Sum() para tipos numéricos devuelve 0. Pero si la colección es de tipos nullable (int?, double?), Sum también funciona bien: ignora los valores null.
5. Media aritmética: Average()
Calculando la media de la clase
Lo típico: ¿cuánto ha sacado de media cada estudiante?
double averageGrade = students.Average(s => s.Grade); // (5+4+3+5+2)/5 = 3.8
Console.WriteLine($"Nota media: {averageGrade:F2}");
Average — tu colega para estadísticas. Ojo: el valor devuelto es siempre double, aunque los valores originales sean int. Así evitas perder la parte decimal.
Si no hay ningún elemento
Si la colección está vacía, llamar a Average() lanza una excepción InvalidOperationException. Es una trampa muy común: si no estás seguro de que hay algo en la colección, ¡compruébalo antes!
if (students.Any())
Console.WriteLine(students.Average(s => s.Grade));
else
Console.WriteLine("¡No hay datos para calcular la media!");
Media en un array de números
double avg = numbers.Average();
6. Máximo y mínimo: Max() y Min()
Quién es la estrella y quién está en el sótano
¿Quieres saber quién tira del carro en la clase y quién... bueno, da algún susto al profe? Fácil:
int maxGrade = students.Max(s => s.Grade); // 5
int minGrade = students.Min(s => s.Grade); // 2
Console.WriteLine($"Nota máxima: {maxGrade}");
Console.WriteLine($"Nota mínima: {minGrade}");
¿Y quién es ese crack?
A veces no solo importa el número, sino el nombre de quien lo ha conseguido. Pillamos el estudiante con la nota más alta:
// Cogemos el primer sobresaliente tras ordenar de mayor a menor
var bestStudent = students.OrderByDescending(s => s.Grade).First();
Console.WriteLine($"Mejor estudiante: {bestStudent.Name} ({bestStudent.Grade})");
En versiones más nuevas de .NET tienes MaxBy, que lo hace más bonito. Desde .NET 6 ya está, y en .NET 9 han añadido más cosas chulas (ver MaxBy en Microsoft Docs). Pero incluso sin MaxBy puedes apañarte con ordenar y .First().
Trampas y diferencias
Si la colección está vacía, llamar a Max() y Min() también lanza una InvalidOperationException. Así que prepárate para eso (sobre todo si no has comprobado la colección antes):
if (students.Any())
Console.WriteLine($"Nota máxima: {students.Max(s => s.Grade)}");
else
Console.WriteLine("No hay estudiantes para buscar el máximo.");
7. Ejemplos de "cadenas" de métodos agregados
Muchas veces los métodos agregados se usan junto con filtrado y proyección:
// Nota media entre los sobresalientes
double avgExcellent = students
.Where(s => s.Grade == 5)
.Average(s => s.Grade); // siempre 5, pero el ejemplo mola
// Suma de notas entre estudiantes con nota al menos 4
int sumGood = students
.Where(s => s.Grade >= 4)
.Sum(s => s.Grade);
// Número de emails únicos (por si acaso)
int uniqueEmails = students
.Select(s => s.Email)
.Distinct()
.Count();
Aquí es donde LINQ empieza a "brillar": combinar operaciones te deja escribir código expresivo y legible, que hasta tu gato entendería (bueno, si tu gato es Junior C# Developer).
8. Comparación con el enfoque "manual": ¿por qué usar agregados?
Para entenderlo, comparemos LINQ con bucles normales usando el ejemplo de calcular la media:
Enfoque clásico:
int sum = 0;
int count = 0;
foreach (var s in students)
{
sum += s.Grade;
count++;
}
double average = (count != 0) ? (double)sum / count : 0;
LINQ:
double average = students.Average(s => s.Grade);
¡Hasta añadir el control de colección vacía es más fácil!
9. Matices útiles
Un poco sobre rendimiento y detalles de implementación
Los agregados de LINQ, a diferencia de la mayoría de operaciones LINQ, se ejecutan al instante. O sea, cuando llamas a Sum(), Count(), Average(), Max(), Min(), el recorrido de la colección ya ocurre en ese momento. Estos métodos no devuelven colecciones, sino un único resultado final.
Esto es importante: si haces algo pesado antes del agregado, como un filtrado o transformación compleja — solo se ejecuta una vez, justo cuando llamas al agregado.
¿Los métodos agregados soportan query-syntax?
Antes de usar métodos LINQ, mucha gente pregunta: "¿Se puede escribir todo esto en query-syntax?" Respuesta corta: el query-syntax no tiene palabras clave para agregados, pero puedes mezclarlos con method syntax:
var avg = (from s in students where s.Grade > 3 select s.Grade).Average();
Dentro de los paréntesis — query-syntax normal, y luego llamas al método agregado. ¡Se hace más de lo que crees!
10. Errores típicos usando LINQ
Error nº1: intentar usar Average(), Max() o Min() en una colección vacía.
Si la colección está vacía, Sum() y Count() devuelven 0 sin problema, pero Average(), Max() y Min() lanzan una excepción. Antes de llamar a estos métodos, asegúrate de que la colección tiene al menos un elemento.
Error nº2: pasar una lambda con la firma equivocada.
Por ejemplo, si pasas una string en vez de un número a una función agregada (Sum, Max, etc.), te dará error de compilación. Es fácil liarse con métodos anónimos.
Error nº3: comprobar el número de elementos de forma poco óptima.
El método Where(...).Count() primero crea una colección nueva y luego cuenta los elementos. Mejor usa Count(predicate) — cuenta directamente los que cumplen la condición y va más rápido.
Error nº4: ignorar los detalles de los tipos nullable al agregar.
Si sumas sobre int?, Sum() ignora los valores null. Es el comportamiento correcto, pero a veces puede darte un resultado inesperado si no lo tienes en cuenta.
GO TO FULL VERSION