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? 😉

Referencias anulables en C# 8.0 y posteriores (y V): atributos de anulabilidad

“I was going nowhere fast,
I was needing something that would last”
Kansas, “Paradox” (1977)

Vuelvo a la carga después de otra larga pausa para terminar la serie sobre las referencias anulables, que se ha extendido en el tiempo mucho más de lo que pensaba originalmente. Parece que voy a tener que darme prisa: ya se prepara la salida (conjuntamente con .NET 6.0 y Visual Studio 2022) de la versión 10 del lenguaje, ¡y yo todavía hablando de cosas que aparecieron en la versión 8! Aunque, como dice la referencia musical de arriba, prisa no tengo mucha, y sí me gustaría publicar algo que pueda servir como guía (y de lo que no me avergüence) dentro de unos cuantos años.

El código de ejemplo inicial en que me basaré hoy es bastante sencillo:

namespace CS90Lib
{
    using System.Diagnostics;
    using System.Linq;
    
    public static class StringFunctions
    {
        public static int GetLetterCount(string? source)
        {
            if (string.IsNullOrWhiteSpace(source))
            {
                return 0;
            }
            Debug.Assert(source != null && source.Length > 0);
            return source.Count(ch => char.IsLetter(ch));
        }
    }
}

Se trata de una pequeña librería de clases que ofrece al mundo una función GetLetterCount para contar cuántas letras hay en una cadena de caracteres Unicode. Ha sido compilada para C# 8.0 bajo un contexto anulable, y el único parámetro de la función es una cadena para la cual el valor null es aceptable.

La entrada de hoy es continuación de la entrega anterior de la serie, y versa sobre los atributos de anulabilidad (nullability attributes), el conjunto de atributos que se añadieron a la librería de .NET como parte del soporte para las referencias anulables. Estos atributos pueden dividirse en dos categorías:

  • Si conoce usted los fundamentos de la arquitectura de .NET Framework y ha visto la especificación más reciente del Lenguaje Intermedio (Intermediate Language, IL) al que el código escrito en C# se transforma, sabrá que IL no ofrece soporte directo alguno para los tipos anulables. Por esta razón, cuando compilamos código que utiliza tipos anulables, el compilador de C# se apoya en dos atributos especialmente definidos en la librería base para anotar la anulabilidad de las entidades usadas en nuestro código (en particular, de los parámetros y valores de retorno de los métodos): NullableAttribute y NullableContextAttribute (espacio de nombres System.Runtime.CompilerServices). Por ejemplo, si se desensambla la librería anterior utilizando ILSpy, se podrá ver que la implementación de la función comienza con las siguientes líneas:
// Methods
.method public hidebysig static
int32 GetLetterCount (
    string source
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
    01 00 02 00 00
)
// ...

Observe que el tipo del parámetro en el encabezamiento a nivel de IL es simplemente string (no string?), y que la función comienza construyendo una instancia del atributo NullableContext, cuyo argumento codifica la posible anulabilidad del parámetro y el tipo de retorno. En este enlace se puede encontrar información detallada sobre qué significa esa codificación.

Estos atributos predefinidos juegan obviamente un rol especial durante la compilación, lo que los convierte de facto en una extensión del sistema de tipos de C#, y no pueden ser utilizados directamente por el programador; por esta razón, que no hablaremos mucho más sobre ellos aquí.

  • Ya hemos hablado en ocasiones anteriores acerca de que el compilador no es omnisapiente, y que frecuentemente (y muy especialmente a través de las fronteras de diferentes ensamblados) es necesario y conveniente suplir la información que el compilador puede detectar (a través de los caracteres ? insertados detrás de los nombres de tipos por referencia) con conocimiento adicional relativo a la anulabilidad o no anulabilidad de las entidades que forman parte de nuestro código. Para ello, la librería base ha incorporado, como parte del espacio de nombres System.Diagnostics.CodeAnalysis, otro conjunto de atributos que permiten suministrar al compilador los hints adicionales necesarios para realizar un mejor análisis de flujo asociado a la anulabilidad. En esta línea, el equipo de desarrollo de .NET viene desde hace algún tiempo anotando detalladamente las librerías con tales atributos.

Supongamos, por ejemplo, que el código original no definiera el parámetro source como string?, sino simplemente como string. Una posible causa podría ser que existe código externo (potencialmente, de terceros) que utiliza actualmente la librería y pudiera estar pasando null como valor al método. En cualquier caso, nosotros, que lo hemos implementado, sabemos que el método incluye una llamada a IsNullOrWhitespace que nos cubre en caso de que el valor null fuera pasado como argumento. ¿Cómo indicarle al compilador que se puede ahorrar las advertencias de anulabilidad asociadas al código cliente, porque nada malo debe pasar? Precisamente para situaciones como ésta es que se ha creado el atributo AllowNull:

namespace CS90Lib
{
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    
    public static class StringFunctions
    {
        public static int GetLetterCount([AllowNullstring source)
        {
            if (string.IsNullOrWhiteSpace(source))
            {
                return 0;
            }
            Debug.Assert(source != null && source.Length > 0);
            return source.Count(ch => char.IsLetter(ch));
        }
    }
}

Así lucirá en este caso el correspondiente código IL:

// Methods
.method public hidebysig static
int32 GetLetterCount (
    string source
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
    01 00 01 00 00
)
.param [1]
    .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.AllowNullAttribute::.ctor() = (
        01 00 00 00 
    )
// ...

Sin el atributo AllowNull, el código que haga llamadas «sospechosas» a la función GetLetterCount recibirá mensajes de advertencia como los siguientes:

CS8604: Possible null reference argument for parameter ‘source’ in StringFunctions.GetLetterCount(string source).

CS8625: Cannot convert null literal to non-nullable reference type.

Con el atributo AllowNull presente, tales advertencias no se generarán.

Al estilo de AllowNull, existe otra decena de atributos que permiten codificar información avanzada de anulabilidad en los metadatos de nuestras entidades. Estos atributos representan, en manos del programador, potentes herramientas con las que expresar contratos a cumplir entre nuestro código y el código que nos llama. Puede encontrar una referencia completa de estos atributos aquí.

Con esta entrada, damos por concluida la serie dedicada a las referencias anulables; habrá que buscar un tema interesante (que siempre los hay) para la próxima. ¡Hasta entonces!

奥克塔维奥


Referencia musical: No me extenderé mucho aquí sobre Kansas, mi banda favorita. «Paradox» es el segundo tema de ese excelente disco que fue el «Point of Know Return» (1977), que llegó hasta el número 4 de la lista Billboard aupado por el éxito del tema «Dust in the Wind«. Si le gusta el grupo y el rock de esa época, le recomiendo que no se pierda el «Point of Know Return: Live and Beyond» (2021), una grabación en directo en la que la formación actual de la banda interpreta el disco original en su totalidad, en conmemoración de su 40-mo aniversario.

Referencias anulables en C# 8.0 y posteriores (IV): el operador ! y las propiedades anulables

“Will you decide what makes you an entity
That’s your identity”
Black Sabbath, “End of the Beginning” (2013)

Después de una pausa que se ha extendido más de lo esperado, dedico hoy un último post al operador ! (null-forgiving operator), sobre el que ya hablamos aquí y aquí. Esta vez quiero presentar otro escenario en el que este operador es de utilidad, con el que me topé recientemente experimentando con Entity Framework para un nuevo pequeño proyecto. El ejemplo que se presenta aquí se basa en un proyecto de aplicación de consola para .NET 5.0 (puesto que necesitamos C# 8 para las referencias anulables), que utiliza el paquete Entity Framework Core 5.0.6 para acceder a un clone de una base de datos a la que tengo mucho cariño: la base de datos FUTBOL2006 que se utiliza en mi libro «C# 3.0 y LINQ».

Las herramientas de Entity Framework Core 5 saben cómo interpretar las referencias anulables que el programador incluya en las definiciones code-first de las entidades:

01  public class Futbolista
02  {
03      public int Id { get; set; }
04      public string Nombre { get; set; } // NOT NULL en el SQL generado
05      public DateTime? FechaNacimiento { get; set; }
06      public string? LugarNacimiento { get; set; } // NULL en el SQL generado
07      public char Posicion { get; set; }
08      public virtual Club Club { get; set; } // propiedad de navegación del modelo
09  }

Al compilar el proyecto, para esta clase se obtienen dos advertencias en las líneas 04 (que corresponde a una propiedad física de la tabla) y 08 (que corresponde a una propiedad de navegación):

CS8618: Non-nullable property must contain a non-null value when exiting constructor.
        Consider declaring the property as nullable.

Por supuesto, la sugerencia de declarar las propiedades como anulables iría en contra de lo que intentamos expresar en primer lugar. En esta página de Microsoft se hace un análisis del problema y se ofrecen posibles alternativas. Una de esas alternativas es precisamente inicializar esas propiedades a null (un valor que nunca se observaría en la vida real) y utilizar el operador ! para acallar al compilador:

04      public string Nombre { get; set; } = null!;

Para terminar, presento a continuación un pequeño programa basado en el modelo anterior que lista los futbolistas que jugaron en la temporada 2006 en el Atlético de Madrid:

static void Main(string[] args)
{
    using var context = new Futbol2006Context();

    // Mostrar futbolistas del Atleti
    var colchoneros2006 = context.Futbolistas.
        Where(f => f.Club.Codigo == "ATM").
        OrderBy(f => f.Posicion).
        ThenBy(f => f.Nombre);

    foreach (var f in colchoneros2006)
    {
        Console.WriteLine("{0} ({1})", f.Nombre, f.Posicion);
    }
}

Ésta es la salida que produce el programa. ¡Cuánto tiempo ha pasado!

ANTONIO LOPEZ (D)
FABIANO ELLER (D)
PABLO (D)
PEREA (D)
PERNIA (D)
SEITARIDIS (D)
ZE CASTRO (D)
AGÜERO (L)
MAXI (L)
MISTA (L)
PETROV (L)
TORRES (L)
COSTINHA (M)
DE LAS CUEVAS (M)
GABI (M)
GALLETTI (M)
JURADO (M)
LUCCIN (M)
MANICHE (M)
VALERA (M)
IVAN CUELLAR (P)
LEO FRANCO (P)

Referencia musical: Black Sabbath ocupa sin lugar a dudas un lugar destacado entre los pioneros del rock y el heavy metal. Aunque luego dejé de oírlos y me decanté por una línea más melódica y espiritual del rock, de adolescente sus discos «Black Sabbath» y «Paranoid» (ambos de un lejano 1970) me impresionaron bastante. El tema «End of the Beginning» (Is this the end of the beginning, or the beginning of the end?) pertenece al último disco de estudio que sacaron, «13» (2013), con el que tal vez paradójicamente llegaron al tope de las listas de éxitos por primera vez.

Referencias anulables en C# 8.0 y posteriores (III): aplicaciones del operador !

[NOTA: Este artículo asume que todo el código que se presenta estará contenido en proyectos que tienen activadas las referencias anulables. Cómo activar las referencias anulables para un proyecto de C# y Visual Studio 2019 se describe aquí].

En la entrada anterior hablamos del llamado null-forgiving operator («operador para perdonar los nulos»), introducido en C# 8.0 como parte del soporte para las referencias anulables (nullable references). Se trata de un operador unario posfijo que se representa mediante un signo de admiración (!), y que informa al compilador de que una expresión de un tipo por referencia no es nula, para que éste no genere las advertencias asociadas a la posible anulabilidad de la expresión.

Según menciona aquí el gran Jon Skeet, autor de «C# in Depth», existen dos escenarios principales en los que el uso del operador ! (que él llama «damn-it», que significa más o menos «¡Maldita sea!») cobra sentido (cito cuasi-textualmente):

  1. Cuando existen invariantes en nuestro código que nos permiten saber más que el compilador sobre la anulabilidad de una expresión. En eso de que «sabemos más que el compilador»  este operador se asemeja a una conversión explícita (cast). Recuerde, no obstante, que no es lo mismo que un cast en términos de comportamiento: no se hace comprobación alguna en tiempo de ejecución.
  2. Cuando se crean test unitarios de validación de argumentos nulos.

Pero en dicho artículo Skeet presenta precisamente una situación que no responde a ninguno de los dos patrones anteriores. Y es que, como ocurre con las meigas, «haberlas, haylas» :-). Como casi siempre, lo importante es conocer bien el lenguaje y aplicar juiciosamente sus características cuando se reconozca la ocasión.

El ejemplo de nuestra entrada anterior es un caso típico del primero de los dos escenarios. Aquí voy a presentar un código algo diferente, aunque igualmente sencillo:

    01  public class Program
    02  {
    03      static void Main()
    04      {
    05          var input = GetName(); // string? GetName() is defined elsewhere 
    06          System.Console.WriteLine(FormatName(input));
    07      }
    08
    09      public static string FormatName(string rawName)
    10      {
    11          if (string.IsNullOrWhiteSpace(rawName))
    12              return string.Empty;
    13
    14          return rawName.Trim().ToUpperInvariant();        
    15      }
    16  }

Suponga que sabemos que GetName, aún cuando su tipo de retorno es string?, nos devolverá siempre una cadena de caracteres no nula. Por su parte, el método FormatName aplica un formato específico a la cadena de caracteres que recibe. Observe que el argumento rawName es de tipo string y no string?, y que funcionará correctamente incluso si se le pasa el valor null como parámetro (aunque esto último ya el compilador tampoco lo sabe). Dado que el tipo de input es formalmente string?, al compilar recibiremos la advertencia:

    CS8604: Possible null reference argument for parameter 'rawName' [line 06]

Como en el ejemplo de la entrada anterior, aquí podremos indicarle al compilador que sabemos más que él y que puede ahorrarse la advertencia aplicando el operador ! al argumento input en la línea 06.

Suponga ahora que queremos escribir una prueba unitaria (unit test) para el método FormatName y comprobar que cuando al método se le pasa null como entrada, la salida es una cadena vacía. La solucion obvia sería la siguiente:

    01  [TestClass] 
    02  public class ProgramTests
    03  {
    04      [TestMethod]
    05      public void FormatNameTest1()
    06      {
    07          // utilice null! para suprimir la advertencia
    08          var result = Program.FormatName(null); 
    09          Assert.IsTrue(result == string.Empty));
    10      }
    11  }

Esta prueba se compila y ejecuta correctamente, pero el compilador nos dará la advertencia:

    CS8625: Cannot convert null literal to non-nullable reference type [line 08]

La advertencia tiene toda la lógica del mundo: si el parámetro es no anulable, ¿para qué vamos a pasarle null a posta? Pero precisamente lo que se quiere aquí es validar el comportamiento del código en esa situación «anormal». ¿Cómo librarnos en este caso de la advertencia? Pues de manera similar a como lo hicimos en el ejemplo anterior: anexando el operador ! a la expresión que se utiliza como argumento de la llamada, en este caso null. Este es un ejemplo típico del segundo tipo de escenarios para la utilización del operador ! a los que se refería Skeet en el artículo antes citado.

En nuestra próxima entrega hablaremos de otro escenario en el cual la expresión null! es de utilidad.

奥克塔维奥

Adiciones recientes a LINQ

“I have to admit it’s getting better
A little better all the time…”
The Beatles, “Getting Better” (1967)

“If I ever lose my faith in you
There’ll be nothing left for me to do…”
Sting, “If I Ever Lose My Faith in You” (1993)

Después de bastante tiempo (una década, para ser más exactos) sin mirar qué nuevos elementos se han incorporado a LINQ (la última vez que lo hice fue aquí), hoy me ha dado por abrir el Object Browser de Visual Studio 2019 y comparar los contenidos de System.Linq.dll para .NET 4.0 y 5.0. Esto no me habría pasado si hubiera seguido manteniendo al día mi libro «C# 3.0 y LINQ», como era mi intención original después que lo escribí. Pero seguro el lector ya sabe por experiencia propia lo malo que es perder la fe. Yo la he perdido en varias cosas (aunque no en la principal, recordando a Sting) a lo largo de mi(s) vida(s).

A continuación presento una pequeña lista con las cosas nuevas que encontré. Dejo para una (posible) próxima entrada un análisis de las diferencias en System.Linq.Expressions.dll. El siguiente es el código de ejemplo asociado:

01 using System; using System.Linq; 
02      
03 class Program
04 {
05     static void Main(string[] args)
06     {
07         int[] a = { 0, 1, 2, 3, 4, 5, 6,  7,  8,  9, 10 };
08         int[] b = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
09  
10         Console.WriteLine(string.Join(", ", a.Prepend(-1).Append(11)));
11
12         Console.WriteLine(string.Join(", ", b.TakeLast(10).SkipLast(5)));
13
14         Console.WriteLine(string.Join(", ", Enumerable.Zip(a, b, (x, y) => x * y)));
15         Console.WriteLine(string.Join(", ", Enumerable.Zip(a, b)));
16         Console.WriteLine(Enumerable.Zip(a, b).First().GetType().FullName);
17     }
18 }

La salida que el programa produce es la siguiente:

-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
1, 2, 3, 5, 8
0, 1, 2, 6, 12, 25, 48, 91, 168, 306, 550
(0, 0), (1, 1), (2, 1), (3, 2), (4, 3), (5, 5), (6, 8), (7, 13), (8, 21), (9, 34), (10, 55)
System.ValueTuple`2[
  [System.Int32, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],
  [System.Int32, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]

1. Prepend y Append

Prepend y Append permiten añadir un elemento al principio o al final de una secuencia, respectivamente, como se desprende del ejemplo. Nada que no se pueda hacer con una o dos líneas de código; pero además de ahorrarnos algunos golpes de teclas, esta incorporación pone a esas funciones en el lugar al que pertenecen por derecho propio.

2. TakeLast y SkipLast

TakeLast y SkipLast son variantes de Take y Skip que toman o descartan elementos del final de una secuencia.

3. Zip sin la función de combinación

Ésta es realmente la única novedad por la que tal vez haya valido la pena haber escrito este post. Precisamente en la entrada a la que hacía referencia antes daba cuenta de la aparición de una nueva función LINQ, Zip, que produce una secuencia aplicando una función de combinación a los elementos correspondientes de otras dos secuencias. Por ejemplo, en la línea 14 del código de ejemplo se utiliza esta versión de Zip para producir la secuencia resultante de la multiplicación de los elementos correspondientes de a y b. Dado que en C# 7.0 se incorporaron al lenguaje las tuplas por valor (value tuples), cobra sentido que se haya añadido más recientemente a LINQ una nueva sobrecarga de Zip que produce una secuencia de tuplas con los elementos correspondientes de las secuencias de entrada. La línea 15 del código muestra un ejemplo, y la línea 16 permite comprobar que el tipo de los elementos de la secuencia es, como cabría esperar, System.ValueTuple.

奥克塔维奥


Referencias musicales:

  • Es prácticamente imposible decir algo sobre The Beatles que no haya sido dicho ya. Solo sé que me siento muy afortunado de haber recibido su influencia en mi temprana juventud. «Getting Better» forma parte de ese disco legendario que es «Sgt. Pepper’s Lonely Hearts Club Band» (1967).
  • Aunque siempre me gustó mucho, y especialmente desde que empezó su periplo en solitario, Sting se convirtió en mi principal referencia musical durante mi vida europea, que empezó en 1992. Precisamente en 1993 Sting publicó el que probablemente es su mejor disco, «Ten Summoner’s Tales«, que contiene «If I Ever Lose My Faith in You», además de otros éxitos como «Fields of Gold», «It’s Probably Me» y «Shape of my Heart».

Referencias anulables en C# 8.0 y posteriores (II): el operador !

“Time keeps flowing like a river…”
The Alan Parsons Project, “Time” (1981)

Ante todo, mis mayores deseos de que este 2021 sea mucho mejor para todos los lectores que el recién terminado 2020.

El tiempo sigue fluyendo (como un río hacia el mar, dijo Alan Parsons), y hace tanto que no escribía una entrada aquí que ya .NET 5.0 y C# 9.0 están disponibles oficialmente. Pero antes de empezar a hablar sobre las  características que se añadieron a la nueva versión del lenguaje, nos quedan todavía algunas cosas dignas de mencionar con relación a las referencias anulables (nullable references), de las que empezamos a hablar en mi post anterior.

Con relación al consumo de librerías que aprovechen esta potente característica, hay que destacar que Microsoft ha hecho un buen trabajo a la hora de anotar las librerías de .NET de manera que cualquier programador que utilice un ensamblado pueda aprovechar las indicaciones que éste nos ofrezca en relación con la anulabilidad de sus argumentos y el valor de retorno. Si bien existían ciertas dudas de que se pudiera entregar un conjunto completo de anotaciones para el lanzamiento oficial de .NET 5.0 y C# 9.0 debido a las limitaciones causadas por el COVID, el reto se cumplió en lo fundamental; en la página de Github Changes to Nullable Reference Type Annotations in .NET 6.0 se habla de un 94% de cobertura actual (.NET 5.0). En ese mismo enlace se detallan las API que falta por anotar y aquellas en las que se han cometido imprecisiones que serán corregidas en .NET 6.0.

En lo relativo a la creación o adaptación de código para hacer uso de esta característica, la vez anterior enumeramos un par de hechos fundamentales:

  • Mientras tanto no se activen las referencias anulables en un proyecto y/o fichero fuente específico, los datos de tipos por referencia en C# 8.0 y posteriores continuarán siendo para el compilador anulables (esto es, capaces de aceptar el valor null) por compatibilidad hacia atrás; pero tan pronto se active la característica, el compilador asumirá que todas las variables, etc. de tipos por referencia son por defecto no anulables, tal como ocurre con los tipos por valor, a menos que se indique explícitamente lo contrario.
  • La acción de «indicar lo contrario» se lleva a cabo añadiendo un signo de interrogación (?) detrás del nombre del tipo en la especificación de la variable, tal cual se hace para los tipos por valor:
           string? middleName;  // puede ser nulo

La otra pequeña adición al lenguaje relacionada con las referencias anulables es el llamado null-forgiving operator (literalmente «operador para perdonar los nulos»), que se describe oficialmente aquí. Se representa mediante un signo de admiración (!) detrás de cualquier expresión de tipo por referencia, y no tiene ningún efecto en tiempo de ejecución: simplemente informa al compilador de que una expresión no es nula, aún cuando el analizador de flujo (que no es omnisapiente) no pueda demostrarlo, y que no debe producir las advertencias asociadas a la posible anulabilidad de la expresión. Es similar a una conversión de tipo explícita (cast) para conversiones anulable/no anulable, con la particularidad de que no se genera comprobación alguna en el código IL. Un ejemplo muy simple podría ser el siguiente:

    01  class Program
    02  {
    03      static void Main()
    04      {
    05          var initValue = GetInitialValue(useNull:false);
    06          System.Console.WriteLine(initValue!.Length);
    07      }
    08
    09      static string? GetInitialValue(bool useNull)
    10      {
    11          return useNull ? null : string.Empty;
    12      }
    13  }

Sin el operador ! de la línea 06, el compilador produciría el mensaje:

    CS8602: Dereference of a possibly null reference [line 06]

Nosotros, que sabemos que GetInitialValue no devolverá null si le pasamos false, podemos aprovechar aquí el nuevo operador para evitar la advertencia.

En general, este operador se debe utilizar con cierta precaución; no obstante, en una próxima entrada presentaremos dos escenarios habituales en los que su uso es conveniente, e incluso imprescindible. ¡Hasta entonces!

奥克塔维奥


Referencia musical: El músico inglés Alan Parsons ya se había asegurado un puesto de primera línea en la historia del pop-rock desde principios de los años ´70, cuando fungió como ingeniero de sonido de varios discos de referencia obligatoria, como «Let It Be» (The Beatles, 1970) o «The Dark Side of the Moon» (Pink Floyd, 1973), entre otros. No contento con eso, creó su propia banda, The Alan Parsons Project, con la que nos ha deleitado desde la segunda mitad de los ´70 hasta nuestros días. Personalmente, mi disco favorito de Alan Parsons es el primero, «Tales of Mystery and Imagination» (1976), con canciones basadas en poemas y cuentos de Edgar Allan Poe como «The Raven» («El cuervo»), «The Tell-Tale Heart» («El corazón delator») o «The Fall of the House of Usher» («La caída de la casa Usher»).

Referencias anulables en C# 8.0: ¿presente o ausente?

“I know you’re here but you’re not really there…”
Kansas, “The Absence of Presence” (2020)

A lo largo de múltiples entradas anteriores hemos venido describiendo las nuevas características que se incluyeron en la versión 8.0 del lenguaje C#, y creo que terminamos a tiempo, ahora que se ha anunciado ya C# 9.0 y se han empezado a desvelar algunas de las futuras posibilidades que ofrecerá (vea, por ejemplo, este enlace). En lo fundamental, solo nos ha faltado hablar de una característica aparecida con C# 8.0, pero que probablemente haya sido la más importante de todas: los tipos por referencia anulables (nullable reference types). He evitado hasta el momento hablar de ellos hasta ahora porque, honestamente, no he tenido la oportunidad de utilizarlas en la práctica por limitaciones de los proyectos en los que he trabajado. En cualquier caso, es importante notar que esta novedosa característica eleva el lenguaje, en mi modesta opinión, a un nuevo nivel, y que incorporarla sobre la marcha a proyectos y existentes requiere, se me antoja, una cierta consideración. En particular, los siguientes enlaces a documentación oficial de Microsoft pueden servirle como guía para ello: Migrate existing code with nullable reference typesChoose a strategy for enabling nullable reference types e Incorporate nullable reference types into your designs.

Los tipos por referencia anulables de C# 8.0 tienen como objetivo superar, de una manera declarativa, una de las principales fuentes de errores de ejecución en los sistemas de programación modernos: las referencias o punteros nulos, que se producen cuando no se ha asignado valor a una referencia o puntero que debe estar «apuntando» a algún lado antes de ser utilizada. Ya dijo el venerable Tony Hoare que inventar las referencias nulas en ALGOL-W (allá por 1965) fue su «billion dollar mistake» (vea, por ejemplo, este vídeo).

Como el uso de los tipos por referencia anulables puede producir rupturas con la compatibilidad hacia atrás del código, por defecto están desactivados. Su utilización se deberá activar explícitamente utilizando opciones o directivas de de compilación, creando lo que se conoce oficialmente como contextos de anulabilidad (nullability contexts). Una vez activa, esta nueva característica del lenguaje permite al programador declarar explícitamente sus intenciones con relación a las variables de tipos por referencia usando la misma notación que ya se utiliza hoy para los tipos por valor anulables: añadiendo un signo de interrogación (?) detrás del nombre del tipo. Por ejemplo, para indicar que una variable de tipo string puede tener el valor null (en un contexto en el que esté activa la compilación con tipos por referencia anulables), se declarará la variable de esta manera:

    string? middleName;  // opcional en USA

En tales contextos, la declaración habitual un tipo por referencia sirve para indicar que la variable o campo no puede nunca tomar el valor null:

    string firstName;  // obligatorio en USA

Cuando se activa la compilación con tipos por referencia anulables, el comportamiento del compilador se modifica con respecto al que ha sido común hasta la versión 7.3 de C# para aceptar la notación antes mencionada y dar soporte a los siguientes dos escenarios:

  1. Si una variable de un tipo por referencia nunca debe tener el valor null, el compilador impondrá reglas de compilación que garanticen que será seguro acceder al objeto al que hace referencia la variable sin necesidad de comprobar antes que la referencia no es nula:
    • La variable deberá ser inicializada con un valor no nulo.
    • A la variable nunca se le deberá asignar el valor null.
  2. Si una variable de un tipo por referencia puede tener el valor null, el compilador utilizará un conjunto de reglas de compilación diferentes para asegurarse de que se haya comprobado que la referencia no es nula antes de acceder al contenido referenciado:
    • Solo se puede acceder al objeto apuntado por la variable cuando se puede garantizar que la referencia no es nula.
    • A estas variables se las puede inicializar con el valor null y se les puede asignar el valor null en otras partes del código.

Un ejemplo básico

Presentaremos los aspectos básicos relacionados con los tipos por referencia anulables utilizando un ejemplo muy sencillo. Para comenzar, crearemos un proyecto de aplicación de consola con C# 8.0, e inmediatamente editaremos el archivo de proyecto, NullableReferenceTypes.csproj, para activar los tipos por referencia anulables:

    <Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    <!-- ... -->
    </Project>

A continuación, editaremos el fichero de código fuente, Program.cs, para que quede de la siguiente forma:

    01  class Program
    02  {
    03      static void Main(string[] args)
    04      {
    05          Greeter myGreeter = null;
    06          myGreeter.Salute("Octavio");
    07      }
    08  }
    09
    10  class Greeter
    11  {
    12      public void Salute(string name)
    13      {
    14          System.Console.WriteLine("Hello, " + name);
    15      }
    16  }

Sin necesidad de compilar, notará que el código anterior tiene dos fallos monumentales, que en C# 7 y anteriores quedarían para ser depurados en ejecución. El compilador de C# 8.0, sin embargo, al ver que la variable myGreeter es de un tipo no anulable (pues no hay signo de interrogación en la declaración, así que estamos en presencia del escenario número 1), nos reportará las dos siguientes claras advertencias. Son solo advertencias; sin embargo, si es Usted como yo, añadirá la línea <TreatWarningsAsErrors>true</TreatWarningsAsErrors> al fichero de proyecto para que todas las advertencias (y éstas en particular) se traten como errores:

    CS8600: Converting null literal or possible null value to non-nullable type 
            [line 05]
    CS8602: Dereference of a possibly null reference [line 06]

¿Y qué pasa si modificamos la línea 05 para indicar que myGreeter acepta el valor nulo (escenario número 2)?

    05          Greeter? myGreeter = null;

Pues en este caso aún verá la segunda advertencia (o error, según la configuración del proyecto):

    CS8602: Dereference of a possibly null reference [line 06]

Aquí el compilador nos advierte de que podría producirse una excepción de referencia nula, porque el analizador de flujo no ve que hayamos comprobado que el valor de la variable myGreeter no es nulo antes de utilizarla. El lector puede comprobar que la advertencia desaparecerá si se añade la condicional correspondiente:

    06      if (myGreeter != null) myGreeter.Salute("Octavio");

Para completar nuestra presentación de hoy, a continuación presentamos una propuesta de cómo «arreglar» el proyecto afirmando no solo la no anulabilidad de la variable myGreeter, sino también la anulabilidad del parámetro name del método SayHello:

    01  class Program
    02  {
    03      static void Main(string[] args)
    04      {
    05          Greeter myGreeter = new Greeter();
    06          myGreeter.Salute("Octavio");
    07      }
    08  }
    09
    10  class Greeter
    11  {
    12      public void Salute(string? name)
    13      {
    14          if (string.IsNullOrWhiteSpace(name))
    15          {
    16              System.Console.WriteLine("Hello, " + name);
    17          } 
    18          else
    19          {
    20              System.Console.WriteLine("Hello, " + name);
    21          }
    22      } 
    23  }

Por supuesto, se trata de un ejemplo trivial que solo roza la superficie de este tema; los tipos por referencia anulables dan para mucho más. En próximas entradas intentaremos profundizar en el contenido de esta entrada y complementarlo.


Referencia musical: Mi banda favorita, Kansas, acaba de añadir a su ya venerable discografía una nueva entrada, «The Absence of Presence» (2020), que recomiendo a todos los amantes del rock clásico y el rock sinfónico. La letra de la canción que da nombre al disco alude a una triste característica de los tiempos que corren: cómo alguien puede estar presente de cuerpo en un lugar, pero mentalmente ausente, concentrado únicamente en lo que emerge de la pantalla de su teléfono móvil. Algo hasta cierto punto parecido a lo que puede pasar con las referencias anulables: uno accede a la variable, pero ésta no apunta a ningún lado.

Sobre stackalloc, o (mejor) Homenaje a John Horton Conway

El pasado fin de semana estaba empezando a escribir este artículo al mismo tiempo que miraba las noticias, que últimamente no traen nada bueno. Ahora que se me van acabando las novedades recientes de C# de las que no he hablado todavía, y que no me atrevo a decir nada sobre aquella de la que no me siento preparado para hablar – léase los tipos-referencia anulables (nullable reference types), el artículo iba a estar dedicado al operador stackalloc, que con la última versión del lenguaje ha salido del oscuro nicho del código no seguro (unsafe code) para pasar a poder ser utilizado en escenarios más comunes.

Esencialmente, al utilizar stackalloc en lugar de new al inicializar un array local dentro de un método, se le está indicando al compilador que reserve la memoria para ese array en la pila de ejecución en lugar de crear un objeto heredero de Array en la memoria dinámica, como ocurriría al usar new. En C# 8.0, ese vector en la pila debe manipularse (fuera de un contexto no seguro) a través de la clase Span<T> de la que ya hablamos hace un tiempo aquí. En muy diversas situaciones, el alojamiento en la pila podría producir mejoras en el rendimiento, ya sea debido a la mayor velocidad de adquisición de la memoria (que en el caso de la pila se reduce a mover un puntero), a la localidad de los datos que el método manipula, o a la disminución en las necesidades de ejecución del recolector de basura.

Una vez claros los fundamentos teóricos en los que se basa la característica, tocaba lo más difícil: encontrar un buen ejemplo que hiciera evidente esas ventajas. Y entonces levanté los ojos para mirar las noticias, donde presentaban a personajes más o menos conocidos que perdieron la vida recientemente a causa del brutal COVID-19. Entre ellos estaba John Horton Conway, el genial matemático británico que creó esa maravilla de máquina universal de Turing que es el Juego de la vida (Game of Life). El Juego de la vida forma parte indeleble de mis memorias de juventud: aún recuerdo el brillo en los ojos de nuestros alumnos de Programación en la Universidad de La Habana cuando les explicábamos1 en qué consiste el juego y luego los veíamos pasarse horas implementándolo en aquellos IBM PC originales (para los que la fecha inicial siempre era el 1 de enero de 1980), utilizando esa otra maravilla de la tecnología de su tiempo que era Turbo PASCAL 3.0 (¿alguien recuerda el número 39671?).

El fichero que se adjunta al final del artículo contiene una implementación en C# del Juego de la vida, adaptada de la que se ofrece en Rosetta Code. El método que crea una nueva generación a partir de la actual es el siguiente:

private void UpdateBoard()
{
    // A temp variable to hold the next state while it's being calculated.
    Span<bool> newBoard = new bool[Width * Height];

    for (var y = 0; y < Height; y++)
    {
        for (var x = 0; x < Width; x++)
        {
            var n = CountLiveNeighbors(x, y);
            var c = board[x + y * Width];

            // A live cell dies unless it has exactly 2 or 3 live neighbors.
            // A dead cell remains dead unless it has exactly 3 live neighbors.
            newBoard[x + y * Width] = c && (n == 2 || n == 3) || !c && n == 3;
        }
    }

    // Set the board to its new state.
    board = newBoard.ToArray();
}

Para adaptarme a las limitaciones de stackalloc, he introducido a Span<T>, he convertido a posta la estructura de datos newBoard (originalmente una matriz) en un array, y me he echado encima el cálculo de la posición en memoria de los elementos. Todo eso hace posible pasar de una versión que usa new para reservar la memoria a una que utiliza stackalloc cambiando sólo la primera línea de código del método:

    Span<bool> newBoard = stackalloc bool[Width * Height];

Con el objetivo de comparar el rendimiento de ambas variantes, el programa de ejemplo ejecuta bajo condiciones repetibles 250 juegos sobre un tablero de 100*100 celdas. Invito al lector a que repita el experimento y vea si obtiene resultados diferentes al mío. En mi viejo Mac, con la optimización de código activada y ejecutando sin depuración, la variante que usa new requirió 694 segundos, mientras que la variante basada en stackalloc consumió 676. No puedo asegurar que estos números den una respuesta concluyente sobre las ventajas de usar stackalloc, pero eso lo dejo también al análisis del lector interesado. A fin de cuentas, el objetivo de este artículo no era tanto hablar de programación como servir como modesto homenaje a la vida y obra de John Horton Conway. God rest his soul!


1 Me refiero aquí a los miembros del equipo docente dirigido por mentor y amigo Miguel Katrib.

Código fuente:

using System;

namespace Conway
{
    // Plays Conway's Game of Life with a random initial state.
    public class GameOfLifeBoard
    {
        // The dimensions of the board in cells.
        private const int Width = 100;
        private const int Height = 100;

        // Holds the current state of the board.
        private bool[] board;

        // Creates the initial board with a random state.
        public GameOfLifeBoard(int randomSeed = 0)
        {
            var random = randomSeed == 0 ? new Random() : new Random(randomSeed);

            board = new bool[Width * Height];
            for (var y = 0; y < Height; y++)
            {
                for (var x = 0; x < Width; x++)
                {
                    // Equal probability of being true or false.
                    board[x + y * Width] = random.Next(2) == 0;
                }
            }
        }

        // Play the game until the colony extinguishes or maximum iterations is reached
        public bool Run(int maxIterations)
        {
            int i = 0;
            while (i < maxIterations & !IsEmpty)
            {
                UpdateBoard();
                i++;
            }
            return !IsEmpty;
        }

        // Return whether the colony has no live cells
        public bool IsEmpty
        {
            get
            {
                for (var y = 0; y < Height; y++)
                {
                    for (var x = 0; x < Width; x++)
                    {
                        if (board[x + y * Width])
                        {
                            return false;
                        }
                    }
                }
                return true;
            }
        }

        // Moves the board to the next state based on Conway's rules.
        private void UpdateBoard()
        {
            // A temp variable to hold the next state while it's being calculated.
            Span newBoard = new bool[Width * Height];

            for (var y = 0; y < Height; y++)
            {
                for (var x = 0; x < Width; x++)
                {
                    var n = CountLiveNeighbors(x, y);
                    var c = board[x + y * Width];

                    // A live cell dies unless it has exactly 2 or 3 live neighbors.
                    // A dead cell remains dead unless it has exactly 3 live neighbors.
                    newBoard[x + y * Width] = c && (n == 2 || n == 3) || !c && n == 3;
                }
            }

            // Set the board to its new state.
            board = newBoard.ToArray();
        }

        // Returns the number of live neighbors around the cell at position (x,y).
        private int CountLiveNeighbors(int x, int y)
        {
            // The number of live neighbors.
            int value = 0;

            // This nested loop enumerates the 9 cells in the specified cells neighborhood.
            for (var j = -1; j <= 1; j++)
            {
                // If y+j is off the board, continue.
                if (y + j < 0 || y + j >= Height)
                {
                    continue;
                }

                for (var i = -1; i <= 1; i++)
                {
                    // If x+i is off the board, continue.
                    if (x + i < 0 || x + i >= Width)
                    {
                        continue;
                    }

                    // Count the neighbor cell at (h,k) if it is alive.
                    value += board[x + i + (y + j) * Width] ? 1 : 0;
                }
            }

            // Subtract 1 if (x,y) is alive since we counted it as a neighbor.
            return value - (board[x + y * Width] ? 1 : 0);
        }

        static void Main(string[] args)
        {
            const int MaxGames = 250;
            const int MaxIterations = 10000;

            DateTime start = DateTime.Now;
            int survived = 0, extinguished = 0;
            for (int i = 0; i < MaxGames; i++)
            {
                Console.WriteLine(i);
                var board = new GameOfLifeBoard(i);
                if (board.Run(MaxIterations))
                {
                    survived++;
                }
                else
                {
                    extinguished++;
                }
            }
            DateTime end = DateTime.Now;

            Console.WriteLine($"Survived:     {survived,6:d}");
            Console.WriteLine($"Extinguished: {extinguished,6:d}");
            Console.WriteLine($"Total time:   {(int)(end - start).TotalSeconds,6:d}");
        }
    }
}

C# 8.0: Enhancements in pattern matching (II)

You can find the original (Spanish) version of this post here.

“Button up your overcoat
When the wind is free
Take good care of yourself
You belong to me!”
Frank Sinatra, “Pick Yourself Up”

In the very first place, I would like to wish all my readers that the perfidious COVID-19 leaves you unscathed, and to encourage you to take care of yourselves and your loved ones – there is so much future ahead of us once we have beaten the obnoxious bug!

In the previous post, where we talked about the enhancements added to the 8.0 version of C# in connection with pattern matching, we left pending a presentation of the new positional patterns; we will try to address that omission here.

Positional patterns allow us to leverage the deconstruction mechanism introduced in C# 7.0 (we talked about it quite some time ago here) to create recursive patterns that combine a type pattern with one or more internal patterns so that the results of the deconstruction of the object subject to matching are recursively matched one by one against those internal patterns. Notice that in C# 7.x, deconstruction could not be used in pattern matching scenarios.

Continuing with the example of the previous post, let’s assume that the Address class includes the following deconstruction mechanism:

public static class AddressExtensions
{
    public static void Deconstruct(this Address address, 
        out string address1, out string address2, out string city, 
        out string state, out string zipCode, out string country)
    {
        address1 = address.StreetAddress;
        address2 = address.StreetAddress2;
        city = address.City;
        state = address.State;
        zipCode = address.ZipCode;
        country = address.Country;
    }
}

Then the method to determine whether a certain address belongs to the Los Angeles downtown, which last time we implemented like this (using a tuple pattern):

public static bool IsDowntownLA(this Address address)
{
    return (address.City, address.ZipCode) is ("Los Angeles", "90012");
}

Could now be also written like this:

public static bool IsDowntownLA_v2(this Address address)
{
    return address is Address(_, _, "Los Angeles", "CA", "90012", _);
}

With the positional patterns and their support for recursion, Microsoft has solved most of the «shallowness» problems in the C# 7.0 pattern matching of which I had complained in the aforementioned post. If, for instance, the City property of Address were an object (with its corresponding deconstructor defined) instead of a simple character string, we would be able to supply a type pattern in the corresponding position, and it would be recursively matched:

public class City
{
    public string Name { getprivate set; }
    public int Population { getprivate set; }

    public City(string name, int population)
    {
        Name = name;
        Population = population;
    }
}
public static class CityExtensions
{
    public static void Deconstruct(this City city,
        out string name, out int population)
    {
        name = city.Name;
        population = city.Population;
    }

    public static bool IsDowntownLA_v3(this Address address)
    {
        return address is Address(_,_, City("Los Angeles",_), "CA", "90012", _);
    }
}

This already looks pretty similar to Prolog’s unification! (although, of course, just in one direction ;-)).

Surely the reader will agree with me in that the original version of IsDowntownLA looks better than the newer ones. The former is more readable, and does not have such as strong dependence on the order (position) of the deconstruction parameters as the other two versions do, which could probably be the source of some future maintenance headaches. But surely you will find scenarios where the associated risks are minimal. For instance, the Microsoft documentation uses in its code fragments the Point class, for which it’s obvious that the deconstruction parameters should be x, y, z (in that order).


Cultural reference: Both Frank Sinatra and Nat King Cole were frequently played in my house when I was a kid, and I’ve never been able to fully break their spell.

C# 8.0: Mejoras en el emparejamiento de patrones (y II)

“Button up your overcoat
When the wind is free
Take good care of yourself
You belong to me!”
Frank Sinatra, “Pick Yourself Up”

Ante todo, quiero desearle a todos mis lectores que el pérfido COVID-19 pase de largo sin tocarles, y exhortarlos a que se cuiden y cuiden a los suyos: ¡hay mucho futuro por delante después que hayamos vencido al odioso bichito!

De la entrada anterior, donde hablamos sobre las mejoras introducidas por la versión 8.0 de C# en relación con el emparejamiento de patrones (pattern matching), nos quedó pendiente la presentación de los patrones posicionales, omisión que intentaremos subsanar ahora.

Los patrones posicionales permiten utilizar el mecanismo de deconstrucción introducido en C# 7.0 (del que hablamos hace algún tiempo aquí) para expresar patrones recursivos que combinen un patrón de emparejamiento por el tipo de datos con uno o más patrones internos en los que se «casen» uno a uno los resultados de la deconstrucción del objeto sujeto a emparejamiento. En C# 7.x, la deconstrucción no se puede utilizar en escenarios de emparejamiento de patrones.

Continuando con el ejemplo de la entrega anterior, supongamos que la clase Address incluye el siguiente mecanismo de deconstrucción:

public static class AddressExtensions
{
    public static void Deconstruct(this Address address, 
        out string address1, out string address2, out string city, 
        out string state, out string zipCode, out string country)
    {
        address1 = address.StreetAddress;
        address2 = address.StreetAddress2;
        city = address.City;
        state = address.State;
        zipCode = address.ZipCode;
        country = address.Country;
    }
}

Entonces el método para determinar si una cierta dirección pertenece al downtown de la ciudad de Los Ángeles, que la vez anterior escribimos así (usando un patrón de tupla):

public static bool IsDowntownLA(this Address address)
{
    return (address.City, address.ZipCode) is ("Los Angeles", "90012");
}

Podríamos también haberlo escrito así:

public static bool IsDowntownLA_v2(this Address address)
{
    return address is Address(_, _, "Los Angeles", "CA", "90012", _);
}

Con los patrones posicionales y recursivos, Microsoft resuelve muchos de los problemas de «superficialidad» en el emparejamiento de patrones en C# 7.0 de los que me quejaba en el artículo antes mencionado. Si, por ejemplo, la propiedad State de Address fuera un objeto compuesto en lugar de una simple cadena de caracteres, se podría especificar un objeto-patrón en la posición correspondiente, y éste sería emparejado recursivamente. ¡Esto ya se va pareciendo bastante a la unificación (unification) de Prolog (aunque, claro, en una sola dirección ;-))!

Por supuesto, el lector coincidirá conmigo en que la versión original de IsDowntownLA es mejor que la nueva. Aquélla es más legible, y ésta tiene una dependencia directa del orden (o posición) de los parámetros de la deconstrucción, cosa que puede darnos algún que otro dolor de cabeza de mantenimiento. Pero seguramente pueden encontrarse escenarios en los que estos riesgos sean mínimos. Por ejemplo, la documentación de Microsoft utiliza en sus fragmentos de código la clase Point, donde es obvio que los parámetros debe ser x, y, z (en ese orden).


Referencia musical: Tanto Frank Sinatra como Nat King Cole se oían muy frecuentemente en mi casa cuando era niño, y aún no he podido librarme totalmente de su embrujo.