Adiciones a LINQ en .NET 6.0

“Flock of angels lift me somehow
Somewhere high and hard and loud
Somewhere deep into the heart of the crowd
I’m the last man standing now
I’m the last man standing now”
Bruce Springsteen, “Last Man Standing” (2020)

A lo largo de estos años, he escrito un par de entradas (aquí y aquí) en las que enumeraba los (relativamente pocos) cambios que se habían ido produciendo en LINQ desde los tiempos en los que escribí el libro «C# 3.0 y LINQ». Con la próxima aparición de .NET 6.0, la tecnología va a recibir probablemente el mayor refresco de su historia (aunque tampoco nada muy espectacular que digamos), a través de las novedades y mejoras que enumeraremos hoy aquí.

El siguiente programa contiene ejemplos de utilización de todas y cada una de esas novedades:

using System;
using System.Linq;

BaseballTeam[] NLWestTeams = new BaseballTeam[]
{
    new BaseballTeam("Los Angeles", "Dodgers", 7),
    new BaseballTeam("San Francisco", "Giants", 8),
    new BaseballTeam("San Diego", "Padres", 0),
    new BaseballTeam("Colorado", "Rockies", 0),
    new BaseballTeam("Arizona", "Diamondbacks", 1),
};

// Devolución de valor arbitrario para secuencia vacía en *OrDefault 
Console.WriteLine(
    (from team in NLWestTeams select team.Name).
    FirstOrDefault(name => name == "Yankees", "(not in league/division)"));

// UnionBy, IntersectBy, ExceptBy, DistinctBy
static string CityStateSelector(BaseballTeam team) => team.CityState;

var s1 = NLWestTeams.
    IntersectBy(NLWestTeams.Select(CityStateSelector), CityStateSelector).
    Select(CityStateSelector);
foreach (var item in s1) { Console.WriteLine(item); } // imprime "Los Angeles"

// MaxBy, MinBy
Console.WriteLine(NLWestTeams.MaxBy(team => team.WSTitleCount)!.FullName);

// Chunk
var s2 = NLWestTeams.Chunk(2);
foreach (var item in s2)
{
    Console.WriteLine(item[0].Name +
        (item.Length > 1 ? " - " + item[1].Name : String.Empty)); // 2 + 2 + 1
}

// Zip sobre tres secuencias
var s3 = NLWestTeams.Zip(NLWestTeams, NLWestTeams);
Console.WriteLine(s3.First());
 
// TryGetNonEnumeratedCount
if (NLWestTeams.TryGetNonEnumeratedCount(out int count))
    Console.WriteLine(count);
else
    Console.WriteLine("Can't efficiently determine the count!");

// Index y Range en ElementAt, ElementAtOrDefault y Take
Console.WriteLine(NLWestTeams.ElementAt(new Index(2, fromEnd: true)));
var s4 = NLWestTeams.Take(1..^2);
foreach (var item in s4) { Console.WriteLine(item.FullName); }

public record BaseballTeam(string CityState, string Name, int WSTitleCount)
{
    public string FullName => CityState + " " + Name;
}

Las novedades en cuestión son las siguientes:

  • Devolución de un valor arbitrario para la secuencia vacía en los operadores FirstOrDefault, SingleOrDefaultLastOrDefault y ElementAtOrDefault.

Ahora los operadores FirstOrDefault, SingleOrDefault, LastOrDefault y ElementAtOrDefault (tanto si se utilizan con o sin una función de filtrado) ofrecen una nueva sobrecarga con un parámetro  adicional que permite especificar el valor a devolver en caso de que la secuencia de entrada no contenga el elemento a buscar. Anteriormente, en tales casos estos operadores devolvían el valor por defecto del tipo de los elementos de la secuencia (null para los tipos por referencia, cero para los tipos enteros, etc.).

  • Nuevos operadores UnionBy, IntersectBy, ExceptBy, DistinctBy que añaden a Union, Intersect, Except y Distinct la posibilidad de especificar una clave para las comparaciones.

Los nuevos operadores UnionBy, IntersectBy, ExceptBy y DistinctBy incluyen un parámetro que permite definir un mecanismo para generar las claves a utilizar en las comparaciones de elementos; por supuesto, se mantiene la posibilidad de especificar también una instancia de IEqualityComparer para personalizar la determinación de igualdad entre dos claves.

  • Nuevos operadores MaxBy y MinBy que complementan a Min y Max añadiendo la posibilidad de especificar el mecanismo de selección de los valores a comparar.

Los nuevos operadores MaxBy y MinBy simplifican la determinación de máximos y mínimos a través de un parámetro adicional para indicar la expresión a maximizar/minimizar.

  • Zip sobre tres secuencias.

El operador Zip (del que hablamos aquí cuando fue introducido) permitía hasta ahora combinar los elementos correspondientes de dos secuencias, enumerándolas simultáneamente. Los diseñadores de la librería parecen haber decidido que la necesidad de operar sobre tres secuencias es lo suficientemente común como para que merezca la pena crear una nueva sobrecarga del operador que permita recorrer simultáneamente y combinar tres secuencias, lo que producirá una mejora de rendimiento apreciable con respecto a si se hicieran dos llamadas consecutivas a la sobrecarga ya existente.

  • Nuevo operador de partición Chunk.

Tal vez la novedad más prácticamente relevante de todas sea la adición de un nuevo operador de partición, Chunk, que (como su nombre indica) permite extraer de una secuencia grupos de elementos consecutivos con la longitud especificada. Se trata de una de esas construcciones que todos los que desarrollamos para .NET Framework hemos tenido que implementar alguna que otra vez; a partir de ahora, tendremos soporte predefinido para ella. Observe que (por razones prácticas, obviamente) Chunk produce siempre una secuencia de arrays del tipo de los elementos de la secuencia de entrada.

  • Nuevo operador TryGetNonEnumeratedCount para garantizar el rendimiento en los conteos.

Como puede comprobarse fácilmente utilizando ILSpy u otra herramienta similar, cuando se utiliza el operador Count de LINQ para determinar la cantidad de elementos en una secuencia, este operador intenta moldear (cast) la secuencia original como una secuencia que implemente ICollection<T> (o sea, que ofrezca una propiedad Count que indique directamente la longitud). Si ello no es posible, el operador se embarca (literalmente :-)) en la enumeración de la secuencia, algo que podría resultar sumamente negativo para el rendimiento, especialmente cuando se opera sobre secuencias no alojadas directamente en memoria. El nuevo operador TryGetNonEnumeratedCount utiliza el patrón de codificación que se ha hecho tradicional en .NET desde la aparición de TryParse, y simplemente devuelve false en caso de que la secuencia original no tenga una propiedad Count, evitando así el coste de la enumeración. Este nuevo operador será de utilidad principalmente para los desarrolladores de librerías en las que uno o más métodos reciban secuencias LINQ cuya naturaleza es desconocida de antemano.

  • Nuevas sobrecargas de ElementAt, ElementAtOrDefault, Take que utilizan los tipos Index y Range.

Los tipos predefinidos System.Index y System.Range se introdujeron en las librerías de .NET Framework cuando apareció el tipo Span<T> (del que hablé en su momento aquí). Ahora estos tipos se incorporan a LINQ como parte de nuevas sobrecargas de los métodos ElementAt y ElementAtOrDefault (en el caso de Index) y Take (en el caso de Range).

Los ejemplos que presento en el código deben ser suficientes para dar una idea del uso de cada una de estas nuevas características. Sobre el programa de ejemplo, note lo siguiente:

  • En él utilizo por primera vez en mi vida el nuevo estilo de escritura de programas sin necesidad de definir explícitamente un método estático Main, característica que se introdujo en C# 9.0 para facilitar una «entrada suave» al lenguaje a los novatos y competir mejor en este aspecto con Swift. Lo único que me ha resultado contraproducente aquí es la necesidad de definir el tipo BaseballTeam al final del archivo de código fuente, para así evitar la advertencia CS8803: Top level statements must precede namespace and type declarations. Por supuesto, podría llevarme la definición del tipo a un nuevo fichero, pero entonces se perdería un poco «la gracia».
  • Note que en la línea de código que ejemplifica el uso de MaxBy he tenido que insertar un operador de «perdonar los nulos» (null-forgiving operator, !), del que hablamos recientemente, para evitar la advertencia CS8602: Dereference a possibly null reference. El compilador no parece actuar muy inteligentemente en esta situación. Está claro que MaxBy podría devolver null si se le suministrase una secuencia vacía, pero es evidente que ése no es el caso aquí. Lo más cercano a una aclaración que he encontrado al respecto es este enlace.

Octavio/Октавио/奥克塔维奥


Referencia musical: Honestamente, Bruce Springsteen nunca me interesó mucho hasta que descubrí, ya bien entrado el milenio, que él era quien había compuesto dos tremendas canciones que yo conocí a finales de los ’70 interpretadas por The Manfred Mann’s Earth Band, «Blinded by the Light» y «Spirits in the Night«. A partir de ahí, empecé a comprar los discos más recientes de The Boss, que son obras de madurez en las que siempre encuentro alguna reflexión interesante expresada por encima de su mezcla tan personal de folk, blues y rock. «Last Man Standing» (algo así como «El último hombre que queda en pie») pertenece al disco «Letter to You» (2020). La idea de utilizarla en esta entrada me sobrevino al darme cuenta de que soy (o al menos, eso parece) uno de los últimos que queda escribiendo su blog en Geeks.ms, sitio que tan bien me ha servido para ese propósito durante más de 15 años. A raíz de esta situación, me surge una pregunta del tipo «huevo y gallina»: ¿nadie escribe porque nadie lee, o nadie lee porque nadie escribe? 😉

Octavio Hernandez

Desarrollador y consultor en tecnologías .NET. Microsoft C# MVP entre 2004 y 2010.

2 comentarios en “Adiciones a LINQ en .NET 6.0”

  1. Tavo,yo como ingeniero geólogo, soy solo un usuario (y muchas veces mall) de software de geociencias. De informática no sé nada. Pero me alegra mucho leerte y ver que eres un desarrollador y consultor de software. Ahhhh, lo que escribes de música si lo sigo y entiendo, aunque no pueda tener toda esa colección de grandes, ni posibilidad de bajarla por internet. Un abrazo, sigue escribiendo, que te seguimos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *