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.

C# 8.0: Mejoras en el emparejamiento de patrones

Hace un par de años, en un artículo publicado en los tiempos de C# 7.x, presentamos los conceptos fundamentales en relación con el emparejamiento de patrones (pattern matching), característica que recién entonces se incorporaba a C# luego de su probado éxito en diferentes lenguajes de programación funcional y lógica (aquí no puedo evitar pensar en Prolog y en quien me lo enseñó, mi querido maestro Dr. Luciano García Garrido, que aún sigue formando profesionales en la Universidad de la Habana). La versión 8.0 de C# ha añadido algunas mejoras con relación a lo que en aquel momento se presentaba como gran novedad; sobre esas mejoras trata precisamente este artículo.

Como bien puede leerse en la página oficial de Microsoft relacionada con las novedades de C# 8.0, el emparejamiento de patrones nos da la posibilidad de expresar funcionalidades dependientes de la forma de los datos; como tal, se trata de un caso particular de programación dirigida por los datos (data-driven programming). La versión 7.0 de C# añadió sintaxis para expresar tres tipos de patrones: los patrones constantes, los patrones de tipo y los patrones var. En un siguiente paso de refinamiento, C# 8.0 ha expandido su vocabulario con nuevos tipos de patrones; además, los patrones, que antes podían utilizarse en dos escenarios, las expresiones is y las sentencias switch, ahora también pueden utilizarse en un tercer contexto: las expresiones switch, de las que ya hablamos aquí. En mi modesta opinión, la referida página de Microsoft no hace un buen trabajo al separar los patrones en sí de los contextos en que pueden utilizarse, cosa que podría llevar a un principiante a confundir los frijoles con la cazuela en que éstos se cuecen ;-). Además, en todos los ejemplos el artículo utiliza expresiones switch; razón por la cual presentaré aquí los nuevos patrones utilizando las otras construcciones.

Los nuevos patrones disponibles en C# 8.0 son los siguientes:

  • Patrones de propiedad: Ahora es posible emparejar una expresión con un patrón usando una propiedad de la expresión. Por ejemplo, si se tiene una clase Address para representar direcciones de correo postal tradicionales, se podría crear el siguiente método para determinar si una dirección corresponde a uno de los estados de la costa oeste de los Estados Unidos:
public static bool IsWestCoast(this Address address)
{
    switch (address)
    {
        case { State: "CA" }: return true;
        case { State: "OR" }: return true;
        case { State: "WA" }: return true;
        default:              return false; 
    }
}
  • Patrones de tupla: Ahora es posible también emparejar una tupla, miembro a miembro, con un patrón de tupla que tenga la misma cantidad de elementos. Este tipo de patrón es útil cuando un algoritmo necesita «casar» varias entradas simultáneamente. Por ejemplo, el siguiente método utiliza una expresión is para determinar si una cierta dirección pertenece al downtown de la ciudad de Los Ángeles:
public static bool IsDowntownLA(this Address address)
{
    return (address.City, address.ZipCode) is ("Los Angeles", "90012");
}

Aunque la documentación oficial a la que antes hacíamos referencia los presenta de manera separada, los patrones de tupla podrían considerarse como un caso particular de los patrones posicionales que se mencionan a continuación.

  • Patrones posicionales: Dejaremos la presentación de los patrones posicionales para la próxima entrega, dado que ésta ya se va haciendo demasiado larga.
  • Por último, vale la pena mencionar que en C# 8.0 los patrones pueden combinarse de manera recursiva. Según la página de Microsoft, «un patrón recursivo es simplemente una expresión-patrón aplicada al resultado producido por otra expresión-patrón». Por ejemplo, suponga que tuviéramos las clases ResidentialAddress y CommercialAddress, derivadas de Address, y quisiéramos definir un método IsCaliforniaBusiness(Address address) para calcular si una dirección corresponde a un negocio o local comercial localizado en California. La versión que se muestra a continuación satisface los requerimientos. En ella primeramente se hace un emparejamiento por el tipo de datos (disponible desde C# 7.0), para luego aplicar un emparejamiento por la propiedad State.
public static bool IsCaliforniaBusiness(this Address address)
{
    return address is CommercialAddress { State: "CA" };
}

15/03/2020: Puede leer la continuación aquí.

C# 8.0: Secuencias asíncronas

Antes que nada, aprovecho para felicitar al lector por la llegada del nuevo año. ¡Le deseo muchas cosas buenas en 2020!

Me permito esta vez alterar lo que sería un orden más natural de presentación de las nuevas características añadidas a C# 8.0 y saltar directamente a una de las más avanzadas, las secuencias asíncronas (async enumerables). Lo hago principalmente como modesto homenaje al cierre de MSDN Magazine, que en su ejemplar final incluye un excelente artículo dedicado al tema, «Iterating with Async Enumerables in C# 8» , de Stephen Toub, disponible aquí. En esta entrega intentaré hacer lo que siempre: no repetir, sino intentar una presentación didáctica y un ejemplo claro y sencillo de utilización de la tecnología en cuestión que complemente a los que pueden encontrarse en las fuentes oficiales. Para acceder a los detalles más técnicos, remito al lector al artículo antes mencionado o a la descripción online de la característica en docs.microsoft.com, que parte de aquí.

En esencia, las secuencias asíncronas hacen posible que un código cliente recorra los elementos de una secuencia utilizando llamadas asíncronas a la hora de producir los elementos de la misma, de modo que el hilo que lleva a cabo el recorrido (enumeración) no quede bloqueado mientras espera por un elemento y pueda llevar a cabo algún otro trabajo útil. Esta posibilidad adquiere una utilidad especial cuando la secuencia a recorrer no es estática o está generada de antemano, sino que se compone dinámicamente a partir de elementos que provienen de una fuente externa, como ocurre en el ejemplo que presentaremos a continuación.

Para hacer posible la implementación de las secuencias asíncronas, .NET Core 3.0 introduce las nuevas interfaces IAsyncEnumerable<T> e IAsyncEnumerable<T> en el espacio de nombres System.Collections.Generic, y la interfaz IAsyncDisposable en el espacio de nombres System. Estas interfaces guardan mucha semejanza con  sus homólogas en el mundo síncrono:

namespace System.Collections.Generic
{
    public interface IAsyncEnumerable<out T>
    {
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken ct = default);
    }

    public interface IAsyncEnumerator<out T>
    {
        ValueTask<bool> MoveNextAsync();
        T Current { get; }
    }
}

namespace System
{
    public interface IAsyncDisposable
    {
        ValueTask DisposeAsync();
    }
}

Observe que la interfaz de enumerador asíncrono no tiene equivalente para el método Reset() de IEnumerator, que ha sido marcado como obsoleto en .NET Core 3.0. Sobre ValueTask y ValueTask<T> ya hablamos en un artículo anterior.

Para producir una secuencia asíncrona en C# 8.0, basta con utilizar la interfaz asíncrona necesaria en la firma del método. Para consumirla, se debe utilizar la construcción await foreach en vez de foreach:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Test
{
    class Program
    {
        static async IAsyncEnumerable<int> FibonacciAsync()
        {
            int n0 = 1, n1 = 1;
            while (true)
            {
                await Task.Delay(100);
                int n2 = n0 + n1;
                n0 = n1; n1 = n2;
                yield return n2;
            }
        }
        static async Task Main(string[] args)
        {
            await foreach(int f in FibonacciAsync())
            {
                Console.WriteLine(f);
                if (f > 100)
                    break;
            }
        }
    }
}

Como ya nos tiene acostumbrados, el compilador se encarga de generar la máquina de estados necesaria para generar la asincronía, y el try/finally (llamando a DisposeAsync, claro) alrededor del recorrido de la secuencia. Puede encontrar más detalles técnicos sobre todo ello en el artículo de Stephen Toub antes mencionado.

Para el ejemplo especial de hoy, debo remitirme a un artículo que escribí para MSDN hace mucho tiempo (febrero de 2008, para ser más exactos). El artículo, llamado «Applying LINQ to new data types«, y que ya solo está disponible aquí (¡gracias a mis buenos amigos de Plain Concepts por ello!), muestra cómo encapsular en un método cliente la recepción de una secuencia de mensajes provenientes de una fuente de comunicación interprocesos conocida como canalización con nombre (named pipe). La solución allí presentada, que se apoya en una secuencia síncrona, podría no ser eficiente, dado que los mensajes podrían llegar a intervalos de tiempo variables, y entonces el hilo que ejecuta el recorrido estaría completamente bloqueado durante esos intervalos esperando la llegada de un mensaje. El uso de una secuencia asíncrona podría venir aquí como anillo al dedo.

La conversión del método que produce los mensajes en un método asíncrono es bastante directa, gracias a que a estas alturas .NET ya está bien preparado para este tipo de programación; en particular, la clase NamedPipeClientStream ofrece variantes asíncronas para la conexión y lectura:

public static async IAsyncEnumerable GetMessagesAsync(
    this NamedPipeClientStream pipeStream)
{
    await pipeStream.ConnectAsync();
    pipeStream.ReadMode = PipeTransmissionMode.Message;

    Decoder decoder = Encoding.UTF8.GetDecoder();

    const int BufferSize = 256;
    byte[] bytes = new byte[BufferSize];
    char[] chars = new char[BufferSize];
    int nBytes = 0;
    StringBuilder msg = new StringBuilder();
    do
    {
        msg.Length = 0;
        do
        {
            nBytes = await pipeStream.ReadAsync(bytes, 0, BufferSize);
            if (nBytes > 0)
            {
                int nChars = decoder.GetCharCount(bytes, 0, nBytes);
                decoder.GetChars(bytes, 0, nBytes, chars, 0, false);
                msg.Append(chars, 0, nChars);
            }
        } while (nBytes > 0 && !pipeStream.IsMessageComplete);
        decoder.Reset();
        if (nBytes > 0)
        {
            // we've got a message - yield it!
            yield return msg.ToString();
        }
    } while (nBytes != 0);
}

El consumo de la secuencia asíncrona por el código cliente es también bastante inmediato:

static async Task Main(string[] args)
{
    const string Server = ".";
    const string PipeName = "CS3";
 
    using (NamedPipeClientStream pipeStream =
      new NamedPipeClientStream(Server, PipeName, PipeDirection.InOut))
    {
        await foreach (var s in pipeStream.GetMessagesAsync())
            Console.WriteLine(s);
    }
    Console.ReadLine();
}

Hasta aquí todo muy bien, pero para poder ir más allá hay un gran PERO: como bien explica Stephen Toub al final de su artículo, ninguno de los dos mecanismos de consultas integradas (LINQ) que ofrecen .NET Core 3.0 y C# 8.0 (los métodos extensores y la sintaxis de consulta) incluye soporte alguno para las secuencias asíncronas; para ello, por ahora, hay que acudir a la librería System.Linq.Async del proyecto github.com/dotnet/reactive. Así que le debo para la próxima, estimado lector, una reescritura del último ejemplo de mi artículo sobre las canalizaciones, en el que filtraba y ordenaba los mensajes recibidos mediante una consulta LINQ. ¡Hasta entonces!


Advertencia: Como casi siempre hago, comencé probando el código presentado aquí en mi Mac, al que tengo mucho apego. Viendo que las canalizaciones con nombre estaban soportadas en .NET Core 3.0, pensé que todo iría de maravillas; sin embargo, al intentar ejecutar el servidor obtuve el error «Message transmission mode is not supported on this platform«. Entonces tuve que cambiarme a Windows :-(.