Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 19 de noviembre de 2024
Nuevos métodos LINQ en .NET 9

Me encanta LINQ. Bueno, ciertamente la sintaxis de consultas integrada en el lenguaje no la uso desde hace muchísimos años, pero los métodos de extensión que proporciona son una maravilla para recorrer y operar sobre colecciones de datos en memoria o almacenados en sistemas externos.

LINQ ha sido uno de los objetivos de .NET 9. Además de mejorarlo internamente y optimizar su rendimiento (en algunos casos, considerablemente), se han incluido tres nuevos métodos, CountBy(), AggregateBy() e Index(), que amplían las posibilidades que teníamos hasta el momento, simplificando la escritura de código y aumentando su legibilidad en algunos escenarios.

Vamos a verlos en detalle.

CountBy()

Hasta .NET 9, no era trivial usar LINQ para contar cuántas veces se repite un valor en una colección. Había primero que llamar a GroupBy() para obtener las agrupaciones, y luego a Count() para contar los elementos, por ejemplo como se muestra a continuación:

var numbers = new[] { 1, 2, 3, 1, 2, 3, 1, 2, 3, 4 };
var counts = numbers
                .GroupBy(n => n)
                .Select(g => new { Number = g.Key, Count = g.Count() });

foreach (var count in counts)
{
    Console.WriteLine($"{count.Number}->{count.Count}");
}
1->3
2->3
3->3
4->1

Con el nuevo método CountBy() podemos simplificar el código anterior, ya que nos permite contar directamente las repeticiones de cada valor en la colección:

var numbers = new[] { 1, 2, 3, 1, 2, 3, 1, 2, 3, 4 };
var counts = numbers.CountBy(n => n);
foreach (var count in counts)
{
    Console.WriteLine($"{count.Key}->{count.Value}");
}

La llamada a CountBy() devuelve un IEnumerable<KeyValuePair<TKey, int>>, o en castellano, una colección de pares clave-valor con las propiedades Key y Value, donde Key es el valor de la colección y Value el número de veces que se repite.

Pero lo que es aún mejor, es que además CountBy() es más eficiente que la combinación de GroupBy() y Count(), ya que no necesita crear un grupo para cada valor, sino que va contando las repeticiones a medida que recorre la colección.

Esto podemos comprobarlo usando de nuestro viejo amigo BenchmarkDotNet, con el que podemos ver que es cuatro veces más rápido y asigna tres veces menos memoria:

| Method       | Mean      | Error     | StdDev    | Gen0   | Allocated |
|------------- |----------:|----------:|----------:|-------:|----------:|
| UsingGroupBy | 28.777 ns | 0.3078 ns | 0.2729 ns | 0.0229 |     192 B |
| UsingCountBy |  6.940 ns | 0.1630 ns | 0.1601 ns | 0.0076 |      64 B |

AggregateBy()

El método AggregateBy() es similar a CountBy(), pero en lugar de contar las repeticiones de cada valor, permite realizar una operación de agregación. Es decir, de la misma forma, nos permite eliminar el GroupBy() intermedio y realizar la operación de agregación directamente.

Para ilustrarlo con un ejemplo, en el siguiente bloque de código vemos una colección de departamentos con el nombre, la ciudad y el número de empleados, y cómo podemos sumar el número de empleados por ciudad usando los operadores de LINQ anteriores

record Department(string Name, string City, int Employees);

Department[] departments =
{
    new ("Sales", "New York", 12),
    new ("Marketing", "Los Angeles", 5),
    new ("Development", "San Francisco", 22),
    new ("Marketing", "London", 3),
    new ("Sales", "Seattle", 8),
    new ("Development", "Sevilla", 1),
    new ("Human Resources", "London", 3),
    new ("HQ", "New York", 3),
};

var employeesByCity = departments
                         .GroupBy(g => g.City)
                         .Select(g => new 
                                    { 
                                        City = g.Key, 
                                        TotalEmployees = g.Sum(d => d.Employees) 
                                    });

foreach (var city in employeesByCity) {
    Console.WriteLine($"{city.City}->{city.TotalEmployees}");
}

El resultado obtenido sería el mostrado a continuación:

New York->15
Los Angeles->5
San Francisco->22
London->6
Seattle->8
Sevilla->1

En .NET 9, podemos simplificar el código anterior usando AggregateBy():

var employeesByCity = departments.AggregateBy(
    d => d.City,                                            // Clave de agrupación
    0,                                                      // Valor inicial (seed)
    (total, department) => total + department.Employees     // Función de agregación
);

// AggregateBy() devuelve una colección de KeyValuePair:
foreach (var city in employeesByCity)
{
    Console.WriteLine($"{city.Key}->{city.Value}");
}

Y como vemos con BenchmarkDotNet, AggregateBy() es diez veces más rápido que la combinación de GroupBy() y Sum(), y no asigna memoria alguna:


| Method           | Mean      | Error     | StdDev    | Gen0   | Allocated |
|----------------- |----------:|----------:|----------:|-------:|----------:|
| UsingGroupBy     | 30.347 ns | 0.6078 ns | 0.6756 ns | 0.0153 |     128 B |
| UsingAggregateBy |  3.092 ns | 0.0252 ns | 0.0211 ns |      - |         - |

Index()

Ahora, imaginemos que, cuando recorremos una colección, necesitamos saber el índice de cada elemento. Hasta .NET 9, la forma de hacerlo era algo engorrosa, ya que había que llevar la cuenta de los índices manualmente, o bien usar consultas LINQ adicionales para conseguirlo. 

Por ejemplo, en el siguiente código vemos cómo podríamos hacerlo en versiones anteriores del framework usando Select() para realizar una proyección de los elementos de la colección junto con su índice en una tupla:


string[] numbers = ["Zero", "One", "Two", "Three", "Four", "Five"];

var numbersWithIndex = numbers.Select((n, i) => (i, n));
foreach (var (index, value) in numbersWithIndex)
{
    Console.WriteLine(index + "->" + value);
}

Como es de esperar, la salida por consola será la siguiente:

0->Zero
1->One
2->Two
3->Three
4->Four
5->Five

Con .NET 9, podemos usar Index() para obtener una tupla como la anterior, con el índice y el elemento de la colección, pero de forma más sencilla y legible:

var numbersWithIndex = numbers.Index();
foreach (var (index, value) in numbersWithIndex)
{
    Console.WriteLine(index + "->" + value);
}

En esta ocasión, BenchmarkDotNet no indica un aumento de rendimiento dramático o un uso de memoria más eficiente, aunque alguna mejora sí que hay:

| Method      | Mean     | Error    | StdDev   | Gen0   | Allocated |
|------------ |---------:|---------:|---------:|-------:|----------:|
| UsingSelect | 66.00 ns | 0.481 ns | 0.450 ns | 0.0143 |     120 B |
| UsingIndex  | 60.48 ns | 0.886 ns | 0.829 ns | 0.0124 |     104 B |

¡Y eso es todo! Espero que el artículo os haya haya servido para descubrir los nuevos métodos LINQ que se han añadido en .NET 9 y que os resulten útiles en vuestros proyectos.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!