Novedades de c# 8

¡Qué fuerte! ¡Qué fuerte! ¡Qué fuerte! ¡Qué Visual Studio 2019 ya está aquí! Bueno, casi. Ahora mismo tenemos disponible la versión Preview 2.2. Promete ser la ostia de rápido. Tiene un montón de novedades para programar en python. Una nueva experiencia para tratar con WorkItems de Azure DevOps. Realmente tiene innumerables nuevas features a la medida del desarrollador moderno. Pero hoy, solo hay una de ellas que nos interese: C# 8.0.

Algunos lo estabais esperando, a otros os la pela al viento: vamos a poner nota a las nuevas características (algunas ya implementadas, otras meras ideas desarrolladas durante las más crueles resacas). Y de la misma forma que ya sucedió con la versión anterior, vamos a usar para ello la escala sexi-loca.

escala sexy loca

Las normas son las de siempre: eje vertical es lo útil que nos resulta y el eje horizontal la locura de su implementación. El objetivo de cada característica es estar por encima de la diagonal Vicky Mendoza (x=y) para poder considerarse una buena característica.

Y esta vez las votaciones no serán producto de mi diarrea mental. Esta vez, son estadísticas bien analizadas de una muestra de desarrolladores válida y contrastada. Durante la pasada <NetCoreConf/> de Barcelona, mi estimado amigo David Gonzalo y un servidor, tuvimos el placer de realizar una encuesta durante la charla “Hot Crazy C#” que tuvimos el placer de presentar (aquí podéis encontrar su artículo relacionado).

No me enrollo más: ¡Al lío!

Huelga decir que algunas de estas funcionalidades no llegarán y otras podrían llegar a publicarse de una forma diferente a la que aquí exponemos.

Nullable reference types

Es por todos conocida la funcionalidad para conseguir que un objeto que no puede ser nulo (como por ejemplo un int), acepte valores nulos. Si añadimos un símbolo de interrogación después de declarar el tipo, este será nullable.

int entero = null; // error
int? entero = null; // ok

Esta nueva funcionalidad trata de identificar valores nulos en tipos que sí que son nativamente nullables (por ejemplo, un string o cualquier otra clase que desarrollemos). La idea es que, si vamos a asignarle valores nulos, lo marquemos con el símbolo interrogante. De esta manera el compilador podrá darnos warnings cuando gestionemos de una forma incorrecta el valor nulo:

string a = null; // Warning: Assignment of null to non-nullable reference type
string? s = null;
WriteLine($"The string is {s}"); // Warning: Possible null reference exception
WriteLine($"The string is {s ?? "null" }");

Es un poco locura tener que marcar como nullable un reference type, pero no obstante añade bastante consistencia al lenguaje, haciendo semejante la gestión de valores null tanto en el heap como en el stack.

Valoración:

Useful = 6.0

Crazy = 3.5

Async streams

Las palabras clave async y await aparecieron en la versión 5.0. y desde entonces existe un problema para iterar con objetos IEnumerable de forma asíncrona.

async Task<IEnumerable<int>> GetManyResultsAsync()
{
    var list = new List<int>();
    int result = -1;
    do {
        result = await GetOneAsync();
        list.Add(result);
    } while (result > 0);

    return list;
}

Al final todo pasaba por materializar completamente todas las llamadas. La propuesta de añadir un tipo IEnumerable que sea asíncrono al iterar es algo que viene fenomenal al lenguaje. La idea es que se pueda parar de materializar resultados en cualquier momento, incluidas las llamadas asíncronas.

async IAsyncEnumerable<int> GetManyResultsAsync()
{
    int result = -1;
    do {
        result = await GetOneAsync();
        yield return result;
    } while (result > 0);
}

En mi opinión es una funcionalidad muy útil, aunque posiblemente el día del evento en el que los asistentes votaron, no supe expresarlo de la forma correcta.

Valoración:

Useful = 7.0

Crazy = 4.0

Range and Index

La idea detrás de los rangos y los índices es copiar ciertas funcionalidades de otros lenguajes de programación. Se centran en interactuar con listas o arrays de una forma más sencilla.

var people = new string[] {
    "ola", "k", "ase", "c#", 
    "ocho", "o", "ke", "hase"
};  

Sin usar Linq recogeremos una porción de este array usando los índices y rangos:

Veamos la funcionalidad en acción:

foreach (var p in people[0..3]) Console.Write($"{p} "); // ola k ase c# 
foreach (var p in people[0..^5]) Console.Write($"{p} "); // ola k ase c# 
foreach (var p in people[^4]) Console.Write($"{p} "); // ocho o ke hase 
foreach (var p in people[6..]) Console.Write($"{p} "); // ke hase 
foreach (var p in people[..]) Console.Write($"{p} "); // ola k ase c# ocho o ke hase 

Otra funcionalidad que parece que va a ser muy interesante a la hora de ahorrarnos dependencias con Linq, aunque lo de que un rango sin índices sea la propia colección de objetos al completo no deja de ser gracioso.

Valoración:

Useful = 5.0

Crazy = 6.5

Recursive patterns

El nombre de esta característica puede resultar un poco confuso. En realidad, hace referencia a Pattern Matching. La idea es que si partimos de una colección de objetos Team donde podemos encontrar objetos de tipo Developer:

IEnumerable<object> Team = ...;

class Developer
{
    public string Name { get; set; }
    public bool IsCrazy { get; set; }
}

Podemos iterar sobre la colección (Team) y en una sentencia if mapear y filtrar las propiedades de cada uno de los elementos. De tal forma que este código:

IEnumerable<string> GetCrazyDevelopers()
{
    foreach (var p in Team)
    {
        if (p is Developer && !p.IsCrazy)
        {
            string name = p.Name;
            yield return name;
        }

    }
}

Lo podríamos transformar en este otro código:

IEnumerable<string> GetCrazyDevelopers()
{
    foreach (var p in Team)
    {
        if (p is Developer { IsCrazy: false, Name: string name }) 
            yield return name;
    }
}

A pesar de ser algo más complejo de primeras, también resulta más compacto. Si te ha costado más de medio minuto entenderlo, es que tu también deberías votar el factor “Crazy” con un valor elevado.

Valoración:

Useful = 5.0

Crazy = 7.5

Switch expressions

Esta característica es semejante a la anterior, con la diferencia de que en este caso vamos a realizar los patrones en sentencias switch y de diferentes formas.

Inicialmente partiremos de la clase Point que simplemente tendrá dos propiedades X e Y:

class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

Y declaramos una variable de tipo object como una instancia de Point:

object o = new Point(X: "10", Y: "5");

El nuevo bloque switch nos va a permitir prescindir de las palabras clave case y default, de tal forma que resumiremos cada opción como una condición y el cuerpo de una función inline:

return o switch
{
    Point p when p.X == 0 && p.Y == 0  => "origin",
    Point p                            => $"{p.X}, {p.Y}",
    _                                  => "unknown"
};

Los más avispados habréis notado que el símbolo “” sustituye a la palabra clave default. Otros estaréis alucinando con la palabra clave when, pero os tengo que decir que eso es de C# 7.0. Lo que todos habremos entendido es que se realiza un _save casting del objeto o a Point y si se cumple, en la primera línea se evalúan unas condiciones, si estas condiciones no se cumplen vamos a la segunda línea y en cualquier otro caso, devolverá “unknown”.

Ahora vamos a complicarlo un poco más y a prescindir de la palabra clave when y a usar un matching semejante al de la característica anterior:

return o switch
{
    Point { X: 0, Y: 0 }         => "origin",
    Point { X: var x, Y: var y } => $"{x}, {y}",
    _                            => "unknown"
};

Pero, ¿y si te dijera que también podemos hacer Pattern Matching con la deconstrucción de un objeto? Aquí pensarás que alguien se ha fumado hierba buena, que es un puto genio o ambas cosas. Vamos a verlo:

Primero tendremos que tener un objeto deconstruible (característica de la versión de C# 7.0.):

class Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public void Deconstruct(out int x, out int y)
    {
        x = X;
        y = Y;
    }
}

Y ahora vamos a sustituir el código anterior por otro tipo de matching sin cambiar el comportamiento:

return o switch
{
    Point(0, 0)         => "origin",
    Point(var x, var y) => $"{x}, {y}",
    _                   => "unknown"
};

Espectacular ¿no?

Quien no le ve utilidad es que está ciego…

Valoración:

Useful = 4.0

Crazy = 8.0

Implicit constructors

Qué sería de una nueva versión de C# en la que no hubiera algún syntax sugar. Esta característica es el primero de ellos. Es muy simple y todos lo entenderemos y usaremos siempre. La idea es inferir el tipo del constructor y así ahorrarnos tener que escribirlo veintisiete veces cuando estemos programando:

Person[] people =
{
    new ("Elena", "Nito", "del Bosque"),
    new ("Armando", "Bronca", "Segura"),
    new ("Dolores", "Cabeza", "Baja"),
    new ("Aitor", "Tilla", "del Bosque"),
};

Si no ves realmente la mejora, piensa que antes, después de cada new pondríamos Person.

Valoración:

Useful = 7.0

Crazy = 2.0

Using declaration

El otro syntax sugar que se está preparando para esta versión de C# es el de los bloques using. Actualmente, si tenemos un objeto que queremos liberar de memoria después de usarlo, lo más recomendable es que implemente la interfaz IDisposable y usarlo dentro de un bloque using. De forma que, al terminar el bloque, el objeto se prepara para ser recogido por el Garbage Collector:

public void Patata()
{
    using (var disposable = CreateDisposable(args))
    {
          ...
    } // disposable is disposed here
}

Para los más vagos, a partir de C# 8.0, ya no tendrán que hacer bloques. Al poner un using, cuando se acabe el ámbito (al finalizar una función o cualquier bloque de código) de “disposeará”:

public void Patata()
{
    using var disposable = CreateDisposable(args);
    ...
} // disposable is disposed here

Una funcionalidad que en dependencia de lo espagueti que sea el código, puede dar muchos quebraderos de cabeza. No obstante, que seamos malos programadores, no hace mala una característica del lenguaje.

Valoración:

Useful = 6.0

Crazy = 2.5

Default interfaces

Llevamos años hablando de esta funcionalidad. A mi juicio una aberración programática que podría ser sustituida por una solución elegante y conocida como es la multiherencia de C++. No obstante Microsoft sigue en sus trece y quiere añadir implementación en las interfaces:

interface ILogger
{
    void Log(LogLevel level, string message);
    void Log(Exception ex) => Log(LogLevel.Error, ex.ToString()); // New overload
}

class ConsoleLogger : ILogger
{
    public void Log(LogLevel level, string message) { ... }
    // Log(Exception) gets default implementation
}

En mi libro de programación orientada a objetos, un artefacto con una implementación por defecto y otra que se debe implementar al heredar se llama clase abstracta, no interfaz.

Por otro lado, otros lenguajes como Java ya traen esta funcionalidad, y Objective-C tiene multiherencia; por lo que podría ser muy interesante de cara a que Xamarin fuera todavía mejor de lo que es:

Por esta razón creo que las votaciones con respecto esta característica fueron más moderadas:

Valoración:

Useful = 4.5

Crazy = 5.5

Conclusiones

Después del evento publicamos una página web con las estadísticas de las votaciones, pero como no creo que la tengamos para siempre online, vamos a hacer unas capturas a continuación:

resultados escala sexy-loca

valor según Diagonal Vicky Mendoza

Las conclusiones son simples, a la gente no le gusta el Pattern Matching: El mundo se va a la mierda.