En este post vamos a ver las novedades que nos trae C# 9; Para poder utilizar C# 9 en cualquiera de nuestros proyectos, debemos instalar la version preview de .NET 5 disponible para descargar en la página de microssoft.

Debemos indicar en nuestro .csproj que utilice esta versión en concreto:

<Project>
 <PropertyGroup>
   <LangVersion>preview</LangVersion>
   <TargetFramework>net5.0</TargetFramework>
 </PropertyGroup>
</Project>

Y finalmente marcar en las opciones Tools-> options ->environments ->preview features Ahí encontrar la casilla de use previews of .NET Core SDK y reiniciar Visual Studio.

 

 

1 - Propiedades init-only

Con las propiedades Init only tenemos la posibilidad de crear object initializers con propiedades inmutables.

Para ello creamos una clase, en nuestro caso Coche pero en vez de crear getters y setters o asignar los valores desde el constructor lo hacemos de la siguiente manera: 

public class Coche
{
    public string Marca { get; init; }
    public string Modelo { get; init; }
}

Como vemos ya no tenemos un setter, sino que tenemos una nueva palabra clave, llamada init. De esta forma podemos utilizarlo como object initializers en c# 9.

Coche coche1 = new Coche { Marca = "Opel", Modelo = "Vectra" };
Coche coche2 = new Coche { Marca = "Audi", Modelo = "A3" };

Console.WriteLine($"Mi primer coche fue un {coche1.Marca} {coche1.Modelo}");
Console.WriteLine($"Mi segundo coche fue un {coche2.Marca} {coche2.Modelo}");

 

1.1 - Campos readonly con init accessors

Cuando utilizamos init accessors únicamente podemos utilizarlos mientras inicializamos el objeto, lo que quiere decir que nos da una funcionalidad similar a readonly. De hecho, si intentamos asignar un valor a un campo con init nos dirá que es read-only.

 

Por este motivo, podemos mutar campos readonly, siempre que lo hagamos desde el init. Anteriormente únicamente podíamos cambiar los valores de los campos readonly desde el constructor, pero ahora lo podemos hacer desde el campo init. 

Debemos cambiar nuestra clase de la siguiente manera:

public class Coche
{
    private readonly string marca;
    private readonly string modelo;

    public string Marca
    {
        get => marca;
        init => marca = (value ?? throw new ArgumentNullException(nameof(marca)));
    }
    public string Modelo
    {
        get => modelo;
        init => modelo = (value ?? throw new ArgumentNullException(nameof(modelo)));
    }
}

 

 

2 - Records en C#

En C#9 vamos a disponer de la estructura record la cual es un tipo (value type) que nos permite encapsular el estado de una propiedad. 

Esto quiere decir que ese objeto o la información que contiene un record será inmutable

 

Para crear un record simplemente debemos cambiar nuestra clase de la siguiente forma:

public record Coche
{
    public string Marca { get; init; }
    public string Modelo { get; init; }
}

 

Como vemos únicamente hemos cambiado el tipo, pero, esto para qué? 

Utilizar records nos permite utilizar nuevas funcionalidades, la idea principal de un record es que este sea inmutable. Por lo tanto si queremos modificar un record lo que debemos hacer es una copia del mismo, y es aquí donde esta funcionalidad cobra sentido.

 

2.1 - Expresión with en records

Con los records podemos utilizar una nueva forma de crear nuevos valores a partir de existentes, lo que haremos será realizar una copia para posteriormente modificar lo que necesitemos. 

Para realizar una copia necesitaremos utilizar la palabra clave with de la siguiente forma:

Coche coche1 = new Coche { Marca = "Opel", Modelo = "Vectra" };
Coche cochedeSustitucion = coche1 with {Modelo = "Astra"};

Console.WriteLine($"Mi primer coche fue un {coche1.Marca} {coche1.Modelo}");
Console.WriteLine($"El coche de sustitución fue {cochedeSustitucion.Marca} {cochedeSustitucion.Modelo}");

 

Como podemos observar utilizamos el objeto que queremos copiar seguido de la palabra clave with y posteriormente la modificación que queremos implementar. 

Como dato interesante decir que los records soportan herencia mientras que struct por ejemplo no lo soporta. 

 

2.2 - Records posicionales

Cuando utilizamos records podemos implementar constructores y deconstructores posicionales, esto quiere decir que las variables se asignan en orden conforme están definidas. 

Si volvemos a nuestro ejemplo del record Coche: 

public record Coche
{
    public string Marca { get; init; }
    public string Modelo { get; init; }

    public Coche(string marca, string modelo) 
      => (Marca, Modelo) = (marca, modelo);
}

Vemos que tenemos nuestro constructor, pero es un tanto diferente a un constructor normal, no solo tiene una expresión lambda lo cual no es habitual, sino que se asignan los valores de una forma un tanto extraña.

(Marca, Modelo) = (marca, modelo);

Como he indicado los valores se asignan dependiendo de la posición, por lo tanto Marca será asignado por marca y Modelo será asignado por modelo. Con el resultado habitual de nuestro console.Writeline:

Mi primer coche fue un Opel Vectra

Por supuesto debemos cambiar la instanciación del objeto a la siguiente: 

Coche coche1 = new Coche("Opel", "Vectra")

 

Esto quiere decir, que si cambiamos el orden de Marca y Modelo en la asignación de los valores estos serán asignados de forma diferente:

(Modelo, Marca) = (marca, modelo);
//Resultado:
Mi primer coche fue un Vectra Opel

 

 

3 - Mejora en los patrones 

Como sabemos en C# 8 vino la mejora de la sentencia swich parece que no es suficiente, y desde el equipo de desarrollo del lenguaje han querido mejorar la sintaxis para que sea mas sencillo de entender a la hora de leerlo.

 

Para este ejemplo he aumentado el record, añadiendo la propiedad CV.

public record Coche
{
    public string Marca { get; init; }
    public string Modelo { get; init; }
    public int CV { get; init; }

    public Coche(string marca, string modelo, int cv) 
      => (Marca, Modelo, CV) = (marca, modelo, cv);
}

 

3.1 - Patrones relacionales

En los patrones relacionales podemos utilizar < > para comprender mayor y menor:

static int CalcularSeguro(Coche coche) =>
  coche.CV switch
    {
        > 100 => 1000,
        < 20 => 100,
        _ => 500,
    };

 

3.2 - Patrones lógicos

Con los patrones lógicos podemos utilizar las palabras claves and, or o not las cuales corresponden a `y`, `o` y `no` como negación.

static int CalcularSeguro(Coche coche) =>
  coche.CV switch
    {
        > 100 => 1000,
        < 20 and > 10 => 100,
        _ => 500,
    };

De esta forma podemos aumentar el nivel de nuestras expresiones.

 

 

4 - Mejora en la expresion new

Como recordamos de C# 8 si especificamos el tipo de variable, a la hora de instanciar un objeto, no necesitamos indicar que tipo es ya que el compilador lo entende dependiendo del tipo de variable.

Coche coche1 = new ("Opel", "Vectra", 90);

 

Desafortunadamente esta funcionalidad no estaba disponible con colecciones de datos, como pueden ser las listas, en C# 9 sí está disponible esta funcionalidad. 

var coches = new List<Coche>
{
    new ("Opel", "Vectra", 90),
    new ("Opel", "Astra", 85),
    new ("Audi", "A3", 130),
};

 

 

5 - Programas de alto nivel

El equipo de desarrollo de C# parece que ha querido llevar el lenguaje un poco más cerca de otros lenguajes de scripting como puede ser python. 

Esto que quiere decir? 

 

Esta es una estructura de una aplicación normal de consola en C# nada más abrir el editor, un hola mundo:

using System;

namespace CSharp9Ejemplo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hola Mundo");
        }
    }
}

 

Como vemos, no tenemos únicamente nuestra sentencia hola mundo si no que tenemos más información, como puede ser el namespace, la clase el método main o incluso los argumentos que se pasan por parámetro.

Esta información es en muchos casos innecesaria, sobre todo cuando estamos aprendiendo. El equipo de C# se ha dado cuenta de esto y ha querido hacer algo al respecto y para ello han creado los top-level programs. Los cuales permiten quitar toda la información que no necesitamos y dejar solo la que si vamos a utilizar, en nuestro caso, el using y la sentencia `Console.Writeline()`

using System;

Console.WriteLine("Hola Mundo");

 

El compilador entiende que es lo que queremos hacer y automáticamente por detrás construye todo lo que necesita, pero es transparente para el usuario.

 

Esta funcionalidad es muy útil para cuando tenemos que hacer pequeños scripts. Pero no está limitada a cosas menores, ya que por ejemplo podemos utilizar cualquier librería del lenguaje como por ejemplo Threading.Task con la que podemos realizar llamadas http en paralelo.