Novedades de C# 8.0 al escenario

El pasado 23 de enero tuve el placer de compartir escenario en la @NetCoreConf de Barcelona con mi amigo @fernandoescolar , y exponer las novedades que nos traerá C# 8.0 en este 2019. Puede que estés pensando “¡uff! eso debió ser una buena siesta y este post de resumen apunta a lo mismo”, así que siendo conscientes de que podría producirse tal situación, decidimos aplicar la escala HotCrazy que en su día hizo Fernando con la versión 7.0, pero en esta ocasión sería la audiencia la encargada de evaluar y poner nota a las diferentes features.

En este artículo explicaré cada una de las novedades y veremos las puntuaciones obtenidas del público.

  • El público en ningún momento ha sido coaccionado para su votación.
  • Los resultados no han sido manipulados (a pesar de los trols).
  • Y ningún gatito ha sido maltratado.

Antes de empezar

Es bueno conocer un poco de historia, tan importante es saber de dónde viene un lenguaje como a donde se dirige. Si estás interesado en los orígenes de C# te recomiendo este artículo donde Fernando lo ha resumido muy bien hasta el día de hoy.

Por otro lado, vamos a comentar brevemente cómo va esto de la escala hot-crazy.

escala sexy loca

Cada feature estará representada en el eje Y por lo útil que nos resulta y en el eje X lo extraño/loco/incomprensible (pon el sinónimo que quieras) que nos parece su implementación. El objetivo es estar por encima de la diagonal Vicky Mendoza (x=y) para poder considerarse una buena feature.


¡Bueno vamos a meternos en harina!

Nullable reference types

El propósito de esta nueva característica es ayudar con la gestión de valores “null” en nuestras variables mediante warnings. La idea es “obligar” a marcar los tipos de referencia, p.ej: string o cualquier otra clase, como nulos haciendo uso del ya conocido símbolo de interrogación ?.

Si ejecutamos este código sobre C#7.0 obtendríamos un error en tiempo de ejecución:

using static System.Console;
class Program
{
    static void Main(string[] args)
    {
        string s = null;
        WriteLine($"The first letter of {s} is {s[0]}"); //Se produce un error porque s es null ---> NullReferenceException
    }
}

Si intentamos nular el tipo string con string? s= null verás que el editor te avisa de que solo puedes nular tipos que sean non-nullable, y string es uno de ellos. Error cuando marcamos null en tipo string en CSharp 7

Y aquí es donde la feature que trae C# 8.0 aparece. Con el código de arriba obtendríamos un warning en la línea que en tiempo de ejecución nos estaba fallando. Lo bueno es que ahora podemos marcar los tipos nulables y que el compilador sepa dónde hacemos uso de ellos, mostrándonos un warning en el caso de no validar correctamente el valor null.

Y el código anterior con C#8.0 nos quedaría así:

using static System.Console;
class Program
{
    static void Main(string[] args)
    {
        string? s = null;
        WriteLine($"The first letter of {s} is {s[0] ?? 'null' }");
    }
}

¿Qué ganamos? Evitar posibles errores en tiempo de ejecución.

Parece que el público se ha dado cuente de ello.

Valoración del público:

Useful = 6.0

Crazy = 3.5

Async streams

Para facilitar el flujo de iteraciones de forma asíncrona en casos donde queremos leer datos sin bloquear la ejecución de un proceso, aparece IAsyncEnumerable, que no es lo mismo que hacer async-await de una tarea que retornará un IEnumerable.

Mejor explicarlo con un ejemplo de código que con tanta prosa.

static async Task Main(string[] args)
{
    foreach(var data in await GetBigResultsAsync())
    {
        Console.WriteLine($"{DateTime.Now.ToString()}  => {data}");
    }
 
    Console.ReadLine();
}
 
static async Task<IEnumerable<int>> GetBigResultsAsync()
{
    List<int> data = new List<int>();
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(1000); //Simulate waiting for external API
        data.Add(i);
    }
 
    return data;
}

En el código de arriba el método GetBigResultsAsync() simula que tarda 1 segundo en obtener un dato numérico. Iterando 10 veces tardaremos 10 segundos en devolver todos los datos en nuestra list. Está sucediendo que desde el primer segundo ya tenemos un dato que podría estar aprovechando los otros 9 segundos para ir procesándose. ¡Estamos desperdiciando un tiempo precioso de poner nuestra CPU a tope!

Pero se ejecuta en bloque y no puedo utilizar yield para devolver el dato. ¿Cómo lo arreglo?

Pues con el nuevo IAsyncEnumerable y el await foreach podemos transformar el código dejándolo tal que así:

static async Task Main(string[] args)
{
    await foreach(var data in GetBigResultsAsync())
    {
        Console.WriteLine($"{DateTime.Now.ToString()}  => {data}"); //Processing data
    }
 
    Console.ReadLine();
}
 
static async IAsyncEnumerable<int> GetBigResultsAsync()
{
    for (int i = 1; i <= 10; i++)
    {
        await Task.Delay(1000); //Simulate waiting for external API
        yield return i;
    }
}

¿Te cuesta verlo? Te ayudará la siguiente animación que compara la ejecución de ambos códigos.

Async Streams new feature Csharp 8

Si quieres ejecutarlo tú mismo, te dejo este ejemplo en mi GitHub:
https://github.com/dagope/chsarp8_async_streams

Valoración del público:

Useful = 7.0

Crazy = 4.0

Rangos e Índices

Los tipos Range e Index llegan con la finalidad de ayudarnos en el manejo de una colección. Se aplican sobre un array o cualquier objeto que cumpla con la interfaz IEnumerator y se declaran entre corchetes los índices de inicio y fin separados por dos puntos seguidos. El índice contiene el valor que nos indica la posición del array a delimitar.

Su sintaxis sería:

Index indexStart = 1;
Index indexEnd = ^5;
Range range = people[2..^5];
Range range = people[indexStart..indexEnd])

Vamos a ver cómo funcionan con el código:

var people = new string[] {
    "Elena", "Armando", "Dolores", "Aitor", 
    "Leia", "Vader", "Yoda", "Skywalker"
};   
foreach (var p in people[0..3]) Console.Write($"{p}, ");    // Elena, Armando, Dolores, Aitor, 
foreach (var p in people[0..^5]) Console.Write($"{p}, ");   // Elena, Armando, Dolores, Aitor, 
foreach (var p in people[^4]) Console.Write($"{p}, ");      // Leia, Vader, Yoda, Skywalker, 
foreach (var p in people[6..]) Console.Write($"{p}, ");     // Yoda, Skywalker, 
foreach (var p in people[..]) Console.Write($"{p}, ");      // Elena, Armando, Dolores, Aitor, Leia, Vader, Yoda, Skywalker,

Vemos que aparece en acción un nuevo símbolo, el ^ circunflejo:
El uso del ^ circunflejo nos indica que nuestro valor del índice comienza desde el final. Si pensamos en que se podría usar el ^0, en realidad estaríamos intentando acceder al elemento siguiente al último y como no hay no puedes llegar a él. Para coger elementos desde el final hacia el principio, debemos empezar a contar los índices desde 1 y no desde 0.

Viendo el ejemplo sacamos las siguientes reglas para los índices:

Valoración del público:

Useful = 5.0

Crazy = 6.5

Parece que no caló muy bien el nuevo símbolo ^ y la gente lo encontró algo más Crazy que Useful.

Recursive patterns

C# cada vez está cogiendo más características de los lenguajes funcionales y esta es una de ellas. ¿te suena Pattern Matching ? Entonces no te costará entender esto.

Partiendo de una clase definida Student:

class Student
{
    public string Name { get; set; }
    public bool Graduated { get; set; }
}

Si tenemos un array de objetos definidos como:

var People = new object[] {
    new Student(){Name = "Leia", Graduated= false},
    new Student(){Name = "Yoda", Graduated= true},
    new Student(){Name = "Skywalker", Graduated= false},
}

En C#7 para obtener los nombres de los no graduados haríamos un foreach con una condición if y devolviendo el nombre del objeto que cumpliese la condición.

Sí, también podríamos usar Linq pero añadimos una dependencia a nuestra clase y tampoco es la finalidad del ejemplo.

El código sería:

IEnumerable<string> GetNameStudentsNotGraduated()
{
    foreach (var p in People)
    {
        if (p is Student && !p.Graduated)
        {
            string name = p.Name;
            yield return name;
        }
    }
}

Y en C#8 lo podemos transformar hacia:

IEnumerable<string> GetNameStudentsNotGraduated()
{
    foreach (var p in People)
    {
        if (p is Student { Graduated: false, Name: string name }) 
            yield return name;
    }
}

Observamos que simplifica bastante la condición del if si pensamos en el filtrado que queremos hacer de nuestra colección. Este debe cumplir que sea un objeto de tipo Student y con Graduated == false. Además, la propiedad Name la asigne a una variable string name que usaré para agregarla con el yield a mi colección de nombres que devuelve mi función.

En mi opinión creo que es bastante más útil que Crazy. Y dada las valoraciones obtenidas parece que la gente optó más por el Crazy.

Valoración del público:

Useful = 5.0

Crazy = 7.5

Switch expressions

Esta característica viene a elevar los bloques switch a su máxima potencia tras haberse metido una buena fumada.

  1. Ahora podremos olvidarnos del case y en su lugar poner la “condición” de varias maneras:
    • Puedo seguir usando mi palabra when, esto ya viene de C#7.
    • Y ¿por qué no un Pattern matching que acabamos de ver antes? Pues sí, puedes y te olvidas del uso de when.
    • Y ¿por qué no haciendo una deconstruccion del objeto? Pues sí, también puedes fumarte eso.
return o switch
{
    Point p when p.X == 5 && p.Y == 5   => "Hight 5",   //Disponible en C# 7.0
    Point { X: 0, Y: 0 }                => "origin",    //Por pattern matching
    Point { X: var x, Y: var y }        => $"{x}, {y}", //Por pattern matching
    Point(-5, -5)                       => "Low",       //Por deconstruccion
    Point(var x, var y)                 => $"{x}, {y}", //Por deconstruccion
    _                                   => "unknown"
};

Ojo: no copies este código tal cual, es probable que la combinación de las cláusulas falle, se muestra a modo de ejemplo

  1. El cuerpo de cada función lo podemos expresar en una misma tras el =>, y de paso también olvidarnos del break; al final. Esto lo compro, me gusta.
var area = figure switch 
{
    Rectangle r => r.Width * r.Height,
    Circle c    => Math.PI * c.Radius * c.Radius,
    _           => 0
};

Bien, pues la imaginación es tu límite con lo que puedes hacer dentro de un switch.

Valoración:

Useful = 4.0

Crazy = 8.0

Era de esperar.

Implicit constructors

Hacía falta algo de azúcar para digerir bien lo anterior y aquí llega un poco. Esta característica cumple con la ley de los vagos de no escribir lo que es evidente.

Si tenemos un array de Personas y queremos inicializarlo, ¿Por qué tengo que poner un new Person(...) a cada elemento si ya el array está tipado? Pues dicho y hecho, ahora podremos omitirlo porque no tiene sentido:

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

Valoración:

Useful = 7.0

Crazy = 2.0

Esto ha gustado ;)

Using declaration

Seguro que estás acostumbrado a utilizar los bloques using para abrir una conexión a base de datos, o en un Stream para la lectura del fichero, etc… en todas esas ocasiones nos aseguramos que nuestro objeto Disposable ejecute su método Dispose() al finalizar el código que engloba el bloque using. Algo cómo:

static void Main(string[] args)
{
    using (var disposable = CreateDisposable(args))
    {
          ...
    } // disposable is disposed here
}

Con esta nueva característica ahora podremos indicar el ámbito del using sobre una variable, sin necesidad de encapsular el código dentro de un bloque using. El método Dispose() del objeto se ejecutará cuando su ámbito finalice.

static void Main(string[] args)
{
    using var disposable = CreateDisposable(args);
    ...
} // disposable is disposed here

Existen algunas limitaciones sobre el uso:

using var stream = file1.Open();
stream = file2.Open();
if (myCustomMethod(
    out using var stream, // Error
    ref size)
)
{
    // our code
}

Valoración del público:

Useful = 6.0

Crazy = 2.5

Parece que tuvo buena aceptación.

Default interfaces

Bueno, hemos llegado a la característica de la polémica. Como si de un debate político se tratase, existen posturas de todos los colores, y se han escrito muchas opiniones al respecto. Se lleva años hablando del tema de si las Interfaces deberían implementar código.

Al final ha llegado, y esto es lo que podemos hacer.

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 opinión, de siempre una interfaz se ha definido como un “contrato” que todo objeto debe cumplir. Si ese “contrato” no lo cumples en su totalidad no es un objeto válido. Bien, pues que ahora una interface pueda incluir código por defecto es como exponer un “contrato” con letra pequeña si no lo cumples. Una letra pequeña que nadie lee o se nos olvida leer en profundidad.

Parece ser que la influencia de lenguajes como Java y Switch a través de Xamarin han acabado trayendo esta característica y nos deja a los programadores la responsabilidad de usarla correctamente.

Valoración del público:

Useful = 4.5

Crazy = 5.5

Había un polizón javero entre el público que seguro moderó los resultados ;-P

Conclusiones

Pues hasta aquí todas las características. Hemos publicado esta página web con las estadísticas de las votaciones, pero como no creo que la tengamos para siempre online, vamos a dejaros unas capturas a continuación:

En etos gráficos podemos visualizar las puntuación de cada feature con la diagonal Vicky Mendoza, como dijimos todo lo que sea por encima era buena señal: Grafico con diagonal Vicky Mendoza

Grafico con resultados positivos negativos según diagonal Vicky Mendoza

Estos son los datos, tuyas son las conclusiones.

Bonus y referencias:

Este artículo tiene su hermano mellizo en este otro escrito por Fernando en su blog. Te recomiendo que lo leas porque aunque se parecen, se complementan.

Y como sabrás, Visual Studio 2019 está al caer y con su presentación llegarán todas estas features de C# 8.0, si algo cambia (cosa que dudo) lo veremos el próximo 2 de abril con la presentación que puedes seguir online.

Y si quieres profundizar más te dejo unas referencias útiles:
https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/
https://blogs.msdn.microsoft.com/dotnet/2019/01/24/do-more-with-patterns-in-c-8-0/
https://vcsjones.com/2019/01/30/csharp-8-using-declarations/
https://dotnetcoretutorials.com/2019/01/09/iasyncenumerable-in-c-8/

Bonus Barcelona NetCoreConf:

Fue un gran evento y creo que nos divertimos dejando la vara de medir al público. Como un gran poder conlleva una gran responsabilidad, decidimos premiar la implicación del público con un obsequio al más Crazy y el más Useful. En secreto diseñamos e imprimimos en 3D dos premios de los que no existen réplicas (de momento). Espero que sus dueños sepan valorarlo y los mantengan a buen recaudo.

El diseño lo he dejado aquí publicado.

Y el momento de la entrega de “premios”:

Agradecer a la organización por haber tenido la oportunidad de participar en un gran evento de comunidad. Nos vemos en otra.

Happy coding!
David.


Comparte esto: