Novedades de C# 9.0

Hace poco, ha sido liberado .NET 5, lo que implica cambios importantes en el mundo de .NET. Uno de estos cambios, es el hecho de que ha llegado a nosotros una nueva versión de C#, la versión 9, con características interesantes que analizaremos a lo largo de este artículo.

Esta publicación, forma parte del 3er. Calendario de Adviento de C#, una excelente iniciativa por Benjamín Camacho. Sin más, vamos a la acción.

Conviértete en un Máster de C# y .NET

Antes de continuar, te invito a que te suscribas a mi academia de entrenamiento para desarrolladores .NET, en la que vas a aprender sobre C#, Blazor, Xamarin, .NET MAUI, ASP.NET, entre muchos otros temas por un mínimo precio.

Video sobre las novedades en C# 9.0

Aquí te dejo el video que realizamos en directo junto con Miguel Teherán, MVP de Microsoft, hablando sobre las novedades de C# 9.0:

Top Level Statements o ( Instrucciones de nivel superior )

Definición

La definición indica, que esta característica “Permite que una secuencia de instrucciones se produzca justo antes de la declaración de un miembro de un espacio de nombres de una unidad de compilación”.

Uso

¿Qué significa esto? Bueno, seguramente, cuando creaste tu primer programa Hola Mundo, o bien, cuando quieres hacer alguna prueba rápida de algún algoritmo, lo que haces es seleccionar una plantilla de Consola para crear tu programa en C#. Cuando esto pasa, puedes observar que se crea una plantilla como la siguiente:

using System;

namespace CSharp9Tests
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Podemos apreciar, que esto nos genera código llamado “boilerplate”, o código pre creado que nos permitirá ejecutar la aplicación. La estructura de la plantilla es la siguiente:

  • usings
  • namespace
  • class
  • method
  • código propio

Las instrucciones de Alto Nivel, lo que nos van a permitir, es reducir la cantidad de código boilerplate, con la finalidad de que reduzcamos la complejidad del punto de entrada de la aplicación.

Es decir, en lugar de tener todo ese código de allá arriba, podemos tener algo como lo siguiente:

using System;

Console.WriteLine("Hello World!");

Así es, la complejidad se reduce considerablemente a tener únicamente un par de líneas, con lo que la estructura del archivo queda de la siguiente forma:

  • usings
  • código propio

También es posible definir métodos, que formarán parte de estas llamadas de alto nivel:

using System;

Console.WriteLine("Hello World!");
Saludar("Héctor");

static void Saludar(string nombre)
{
    Console.WriteLine($"Hola {nombre}");
}

Ahora bien, ¿A qué se refiere la parte de “produzca justo antes de la declaración de un miembro de un espacio de nombres de una unidad de compilación”? Bueno, principalmente, a que las declaraciones de alto nivel deben estar definidas antes de la declaración de un miembro de un espacio de nombres, como, por ejemplo, una clase.

Si declaramos una clase, y a continuación tratamos de escribir más instrucciones de nivel superior, recibiremos un error:

using System;


await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");

public class Customer
{
    public string Name { get; set; }
}

System.Console.WriteLine("Hi!");

Consideraciones

Cabe destacar, que las instrucciones de nivel superior, únicamente funcionan con el archivo de Punto de Entrada (regularmente conocido como Program.cs).

Init Only Setters ( Establecedores de solo inicialización)

Definición

Esta es otra característica de C#, la cual, agrega el concepto de propiedades e indizadores de solo inicialización a C#. 

Uso

Existen ocasiones, en las cuales, deseamos que una clase tenga una propiedad que pueda ser inicializada durante su construcción, pero que no permita el cambio de valores una vez que ha sido creado el objeto. Muchas veces, esto se logra estableciendo un set privado, y creando un constructor, el cual modifica los valores mientras se crea la instancia.

Con C# 9, esto se acabó, y contamos con una forma de lograr este objetivo, sin tener que estar escribiendo constructores para inicializar un objeto. Tan sólo, tenemos que utilizar la palabra clave init, de la siguiente forma:

    public class Customer
    {
        public int CustomerId { get; init; }
        public string Name { get; set; }
    }

De esta forma, será posible asignar un valor, cuando construyamos el objeto, como por ejemplo:

var instance = new Customer { CustomerId = 1, Name = "Héctor" };

O a través de un constructor:

        public Customer()
        {
            CustomerId = 50;
        }

Pero no será posible modificar sus valores, una vez que el objeto haya sido creado.

Consideraciones

Debes asegurarte de inicializar estas propiedades durante la creación del objeto, ya que si se te olvida esto, tendrás en tus propiedades, los valores por default de acuerdo al tipo de dato.

Target-typed new expressions (Expresiones de tipo de destino new)

Definición

Con esta característica de C# 9, podemos evitarnos el tener que especificar el tipo para los constructores cuando hemos declarado el tipo.

Uso

La definición suena un poco enredada, pero el uso es más fácil de lo que imaginas. Antes de C# 9, podíamos crear objetos de las siguientes formas:

Customer c = new Customer();

O también de la siguiente forma:

var c = new Customer();

Bueno, con esta feature, lo que podremos hacer, es omitir el nombre del tipo en la parte de la derecha del enunciado, siempre y cuando conozcamos el tipo en la parte de la izquierda. Es decir, podremos hacer algo como lo siguiente:

Customer c = new ();

O incluso, si tenemos varias propiedades, podremos inicializarlas con un constructor:

Customer c = new ("Héctor");

O con una inicialización de propiedades:

Customer c = new() {Name = "Héctor" };

Consideraciones

Es necesario conocer el tipo de dato en la parte de la izquierda, y no olvidarse del uso de los paréntesis en la parte de la derecha del enunciado.

Patrones Relacionales

Definición

Los patrones relacionales, permiten a los programadores indicar que un valor de entrada debe cumplir una restricción relacional en comparación con un valor constante.

Uso

Supón que debes crear una pequeña aplicación, donde el usuario deba introducir su edad, para que la aplicación despliegue lo siguiente:

0-3 -> Infante

4-12 -> Niño

13-17 -> Puberto

18-30 -> Joven

30-65 -> Adulto

Más de 65 -> Tercera Edad Si te dijera que debemos resolver el problema con una sentencia switch, tal vez pensarías que no es la mejor opción. Sin embargo, esto se vuelve sumamente sencillo con los patrones relacionales:

        switch (edad)
        {
            case > 0 and <= 3:
                Console.WriteLine("Infante");
                break;
            case >= 4 and <= 12:
                Console.WriteLine("Niño");
                break;
            case >= 13 and <= 17:
                Console.WriteLine("Puberto");
                break;
            case >= 18 and <= 30:
                Console.WriteLine("Joven");
                break;
            case >= 30 and <= 65:
                Console.WriteLine("Adulto");
                break;
            case > 65:
                Console.WriteLine("Tercera Edad");
                break;
        }

Consideraciones

Tal vez encuentres otro tipo de ejemplos de esta característica, como este ejemplo, donde se crea un método estático:

    public static LifeStage LifeStageAtAge(int age) => age switch
    {
        < 0 =>  LifeStage.Prenatal,
        < 2 =>  LifeStage.Infant,
        < 4 =>  LifeStage.Toddler,
        < 6 =>  LifeStage.EarlyChild,
        < 12 => LifeStage.MiddleChild,
        < 20 => LifeStage.Adolescent,
        < 40 => LifeStage.EarlyAdult,
        < 65 => LifeStage.MiddleAdult,
        _ =>    LifeStage.LateAdult,
    };

El _, permite especificar un caso en el que no se cumpla ninguna de las condiciones anteriores (como un default en un switch normal).

Records (Registros)

Definición

Los records, son tipos por referencia inmutables, y que proveen características para comparar sus valores.

Uso

Llegamos a la que considero, característica más importante introducida en C# 9. Se trata de los records ó registros.

Para crear un record, se debe emplear la palabra clave “record”, es algo similar a como si creáramos una clase:

public record Person
{
    public string Name { get; set; }
}

Si creamos un objeto de este tipo, y cambiamos una de sus propiedades, no pasa nada, y es como si hubiéramos creado una clase cualquiera:

Person p = new() { Name = "Héctor" };
p.Name = "Juan";

Para habilitar la parte de inmutabilidad, debemos utilizar otra característica de C# 9 que hemos visto anteriormente, los establecedores de solo inicialización, ó inits para los amigos. En cuanto hacemos esto, inmediatamente salta un error:

Una forma simplificada, para activar de forma automática esta característica en cada propiedad, es definir el tipo de la siguiente forma:

public record Person(string Name);

Una característica de este tipo de dato (entre muchas otras), en comparación de una clase, es si utilizamos el método ToString() por default. Mira el resultado de utilizarlo en uno y en otro:

Clase clase = new() {Name = "Héctor" };
Registro registro = new("Héctor");

Console.WriteLine(clase);
Console.WriteLine(registro);

Resultado:

Otra característica, es que si hacemos comparaciones en este tipo de objetos, se nos devolverá la comparación de los valores, y no de la referencia como tal, como sucede con una clase:

Clase clase = new() {Name = "Héctor" };
Clase clase2 = new() { Name = "Héctor" };
Registro registro = new("Héctor");
Registro registro2 = new("Héctor");

Console.WriteLine(clase.Equals(clase2));
Console.WriteLine(registro2.Equals(registro2));

Esto es algo muy interesante, ¿A poco no?

Consideraciones

Tal vez te preguntes, ¿Qué es un objeto inmutable? ¿Y para qué son buenos los records? Bueno, un tipo inmutable, básicamente es un tipo que no cambia. Un string por ejemplo, aunque no lo creas, en realidad es inmutable. Cada que asignas un nuevo valor a una cadena, en realidad se crea una nueva instancia del tipo de dato string y se hacen los ajustes para almacenar el nuevo valor.

Es, por lo tanto, una forma en la que podemos asegurarnos, que un objeto no sea modificado en un lugar donde no queremos.

Uno de los mejores ejemplos donde es útil utilizar objetos inmutables, es en la concurrencia, ya que no importará el lugar desde donde accedamos al objeto, ya que siempre tendrá los mismos valores.

Deja un comentario

Tu dirección de correo electrónico no será publicada.