Novedades de c# 10

19 May 2021 · 14 mins. de lectura

Hace unos días Microsoft anunció Visual Studio 2022. La herramienta que viene a actualizar al ya vetusto Visual Studio 2019 (dos años en el mundo de la tecnología son una eternidad). Dicen que podremos probarlo en verano, pero no han indicado fecha exacta de lanzamiento. Quizá lo publiquen junto con .Net 6 en noviembre. Y si sacan nueva versión de IDE y de framework, tendrán nueva versión de C#. Digo yo…

¿Lo llamarán C# X? ¿O se decantarán por un nombre menos dinámico y más aburrido como C# 10?

Lo que sí que sabemos es que, con cada versión nueva de nuestro lenguaje de programación favorito, tenemos nuevo artículo de análisis. De análisis y evaluación. Y para ello usaremos la famosa escala sexy-loca.

escala sexy loca

Las normas de puntuación son las habituales:

Este año nos hemos adelantado un poco, así que algunas de estas funcionalidades no llegarán a aparecer y otras podrían llegar a publicarse, pero de una forma diferente a la que vamos a exponer.

Global Using

Imagínate poder crear un archivo dentro de tu aplicación donde declaras las cláusulas using que vas a usar a lo largo de tu código fuente. Una declaración global que referenciaría paquetes sin necesidad de hacerlo en cada uno de los archivos de tu proyecto. Esa es la idea de global using:

// Imports.cs
global using System;
global using static Developerro.Constants;
global using Alias = Developerro.MyClass;

Si declaramos una referencia, un conjunto de variables o un alias de forma global; estará disponible a lo largo de todo el código fuente del proyecto sin necesidad de tener que volver a referenciarlo archivo a archivo.

Con esta característica he de admitir que limpiaría muchos using del principio de muchos archivos. De esta manera podríamos ganar visibilidad del código fuente. También quedarían más evidentes todas las dependencias, al estar juntas en un solo lugar. Y si no nos interesa y queremos seguir referenciando paquetes en cada fichero, solo tenemos que ignorar que existe.

Solo espero que no empiecen a aparecer estos global using esparcidos por diferentes archivos y se convierta en una locura conocer las dependencias de un código.

Valoración:

Useful = 7

Crazy = 3

Constant interpolated strings

Las cadenas de texto interpoladas (o “el format del dolarín” como le llamo yo) han añadido a C# la capacidad de escribir la línea de código más elegante que he encontrado en mi carrera:

var wtf = $@"%!&#";

La unión perfecta entre una viñeta de Ibáñez y la programación.

Esta característica va de poder realizar este tipo de asignación en una variable constante, y ya que estamos, realizar composición:

const string baseUrl = $"https://www.developerro.com";
const string blogUrl = $"{baseUrl}/blog";
const string videosUrl = $"{baseUrl}/videos";

Una funcionalidad que a muchos nos ayudaría. No obstante, hay que tener en cuenta un problema. Si por ejemplo definimos:

const string pi = $"{3.14}}";

Da la casualidad de que esto no podría ser un valor constante. Quizá debería dar error. Cuando convertimos un valor numérico con decimales a una cadena de texto, se utiliza la cultura actual para dar formato al número. Por ejemplo, ese código en un ordenador con cultura española escribiría "3,14" pero con cultura inglesa "3.14". Así que ¿cuál sería la cultura para crear esta constante? El caso es que hoy en día, este hecho, tiene que afectar al factor Crazy de nuestra valoración.

Valoración:

Useful = 8

Crazy = 5

Record structs

El tipo record fue la funcionalidad estrella de C# 9. Es normal que se quiera extender su uso para objetos de tipo struct:

public record struct MyRecordStruct(int Id, string Name);

Mientras que una clase record podríamos decidir marcarla o no:

public record class MyRecordClass1(int Id, string Name);
public record MyRecordClass2(int Id, string Name);

Este tipo de objetos está destinado principalmente a ser inmutables, añadir deconstrucción y tener una forma de declaración concisa. Actualmente son tipos de referencia, un puntero a memoria. Resulta totalmente lógico querer dotar a los tipos valor (que se almacenan en el stack) de estas ventajas.

No obstante, aún no he llegado a encontrar ese escenario en el que usar este tipo de objetos me supondría una ventaja. Y esto unido con una declaración de este tipo:

public abstract record class MyClass(int Id);

Hace que no pueda ser tan bondadoso con esta característica como lo fui con su antecesora.

Valoración:

Useful = 5

Crazy = 5

Static abstract members in interfaces

Cuando hablamos de una nueva versión de C#, el tema de dar más funcionalidad a una interface de la que ya tiene, sigue en el candelero.

Esta vez le toca a poder declarar métodos estáticos:

interface IAddable<T> where T : IAddable<T>
{
  static abstract T Zero { get; }
  static abstract T operator +(T t1, T t2);
}
struct Int32 : ..., IAddable<Int32>
{
  static Int32 I.operator +(Int32 x, Int32 y) => x + y;
  public static int Zero => 0;
}

Como podemos ver en el ejemplo anterior, la idea sería poder obligar a implementar operadores o incluso tener un operador ya implementado por defecto en nuestra interface.

De hecho, esta funcionalidad podría servir para declarar interfaces para métodos estáticos de todo tipo:

public interface IAsyncFactory<T>
{
  abstract static Task<T> CreateAsync();
}

Pero a parte de su uso para operadores, imaginemos este código:

public interface IExample
{
  string DefaultInstanceProperty => "default instance property";
  static string DefaultStaticProperty => "default static property";
}

Aquí hemos declararíamos dos propiedades con comportamiento por defecto: una que usaríamos en forma de instancia y la otra de forma estática.

Si queremos usar y sacar partido de esta funcionalidad podría crear la siguiente clase:

class MyGenericClass<T> where T : class, IExample
{
  public MyGenericClass(T t)
  {
    var a = t.DefaultInstanceProperty;
    var b = T.DefaultStaticProperty; // ???
    var c = t.DefaultStaticProperty; // ???
    var d = IExample.DefaultStaticProperty;
  }
}

La asignación de la variable b o c ¿sería correcta? Es una pregunta al aire porque las personas involucradas en la mejora de C# no han encontrado una respuesta todavía.

Una característica que podría ser muy interesante, pero que, como siempre que damos una mayor funcionalidad a una interfaz, podríamos solucionar usando clases abstractas junto con multiherencia.

Valoración:

Useful = 7

Crazy = 9

File scoped namespaces

Si has programado en java sabrás de qué va esto.

Lo cierto es que un archivo de código en C# suele estar vinculado con un solo namespace. No obstante, nos vemos obligados a usar un bloque de código encapsulado entre llaves, para indicar al compilador que estamos dentro del contexto de ese namespace:

using System;

namespace Company.Project
{
  public class Product
  {
    /*...*/
  }
}

Si decidiéramos poner la declaración en una sola línea y que, a partir de ahí, todo el código que escribamos dentro del archivo perteneciera a ese namespace, podríamos exponer el código anterior de esta forma:

namespace Company.Project;
using System;

public class Product
{
    /*...*/
}

Quitaríamos una indentación de la clase y sería mucho más legible el código. También sería interesante poder escribir nuestro ejemplo en este formato:

using System;
namespace Company;
namespace Project;
public class Product
{
  /*...*/
}

Es una funcionalidad que nos gusta. No nos soluciona la vida, pero está bien. Y es una de las pocas cosas en las que se podría considerar que java es superior a C#.

Valoración:

Useful = 7

Crazy = 1

Lambda improvements

Aquí llegamos a la nueva funcionalidad estrella de esta versión: las mejoras de las lambdas. El objetivo es poder usar Asp.Net Core y declarar endpoints de esta forma:

app.MapAction([HttpGet("/")] () => new Todo(Id: 0, Name: "Name"));
app.MapAction([HttpPost("/")] ([FromBody] Todo todo) => todo);

Los amigos de Redmond llevan días publicando en las redes sociales las bondades de esta nueva forma de usar las expresiones y lo bonito que será que una aplicación completa quepa en un solo tweet. Sin embargo, para conseguirlo hay que implementar tres características:

Allow lambdas with explicit return type

Poder especificar y/o inferir el tipo devuelto por una expresión lambda:

var f4 = () => 1;              // System.Func<int>
var f5 = () : string => null;  // System.Func<string>

Parece una solemne tontería y alguno podría pensar que ya estaba implementado en versiones anteriores, pero nada más lejos.

Infer a natural delegate type for lambdas and method groups

Inferir el tipo System.Delegate a una expresión lambda. Esto es que toda expresión herede y/o tenga conversión implícita al tipo Delegate. Este tipo es a su vez un object y puede ser explorado vía Reflection:

Delegate d1 = 1.GetHashCode;
Delegate o1 = (int x) => x;
object o1 = (int x) => x;

Otra funcionalidad que muchos pensarán que ya estaba implementada, y no era así. Para conseguir un casting a Delegate, en versiones anteriores del lenguaje teníamos que hacer alguna virguería:

Delegate d1 = () => "ey!";                   // error
Delegate d2 = new Func<string>(() => "ey!"); // ok
Delegate d3 = (Func<string>)(() => "ey!");   // ok
Delegate d4 = (string s) => { };             // error
Delegate d5 = (Action<string>)((s) => { });  // ok

delegate void ActionDelegate(string s);
Delegate d6 = (ActionDelegate)((s) => { });  // ok

Comparándolo, queda claro que conseguimos mucha más claridad en el código…

Allow lambdas with attributes

Para poder copiar el funcionamiento de una acción de un controlador en una sola línea, lo único que faltaría sería poder añadir atributos dentro de una expresión lambda:

var controllerAction = [HttpGet("/")]([FromBody] Todo todo) => todo

En conjunto es una funcionalidad que nos trae muchas mejoras. No se verán quizá en el día a día, pero sí en los frameworks y herramientas que desarrollamos. Sin embargo, el tema de los atributos es muy loco. Y que sea solo para dar soporte a una demo que consiste en declarar una aplicación en un solo archivo, más todavía.

Valoración:

Useful = 10

Crazy = 10

Allow Generic Attributes

Una de las funcionalidades que más se han demandado y que no terminan de implementar es la del uso de argumentos genéricos en atributos. Decirlo es complejo, pero veamos un ejemplo:

public class SomeAttribute<T> : Attribute
{
  /*...*/
}

public class GenericClass<T>
{
  [SomeAttribute<T>]
  public void DoSomething(T input)
  {
    /*...*/
  }
}

A muchos les habrá venido a la cabeza el ejemplo de describir las respuestas OpenAPI:

public class TodosController : Controller
{
  [ProducesResponseType(200, typeof(Todo))]
  [ProducesResponseType(400)]
  public Task<IActionResult> GetById(int id)
  {
    /*...*/
  }
}

Hasta aquí todo bien, porque especificamos la respuesta de la acción GetById como código 200 y del tipo Todo. Pero cuando creamos una base común para todas nuestras APIs nos encontramos un problema, el parámetro genérico que solemos usar:

public class MyBaseController<TEntity> : Controller
{
  [ProducesResponseType(200, typeof(TEntity))] // error
  [ProducesResponseType<TEntity>(200)]         // new feature
  [ProducesResponseType(400)]
  public virtual Task<IActionResult> GetById(int id)
  {
    /*...*/
  }
}

Aunque hay alternativas a este problema (como por ejemplo usar un objeto tipo IActionResult<T>), sí que es verdad que nos vendría muy bien esta nueva alternativa.

Lamentablemente, los atributos se serializan en los ensamblados con el formato de texto usando el assembly fully qualified name. Si queremos mantener este mismo formato o que sea compatible con versiones anteriores, tendremos un problema. Eso sí, si tienes la solución no dudes en contactar con el equipo de Roslyn.

Valoración:

Useful = 8

Crazy = 7

Deconstruction improvements

Otro de los campos que se tiene previsto mejorar es el de la deconstrucción. Y se plantean dos nuevas características:

Mix Declarations and Variables

Actualmente, los constructores no nos dejan mezclar variables declaradas o no. Así que, tenemos que definirlos antes de realizar la llamada:

(int, bool) MyFunction() => (10, true);

bool wasOk = false;
int result1;
(result1, wasOk) = MyFunction();
if (wasOk) return (result1, wasOk);

int result2;
(result2, wasOk) = MyFunction();
if (wasOk) return (result2, wasOk);

Pero si pudiéramos mezclar parámetros declarados y no declarados, podríamos hacer lo que hace este código de una forma más escueta:

bool wasOk = false;
(var result1, wasOk) = MyFunction();
if (wasOk) return (result1, wasOk);

(var result2, wasOk) = MyFunction();
if (wasOk) return (result2, wasOk);

Default deconstruction

Por otro lado, tenemos el tema de default. Si queremos definir una tupla para que tenga los valores por defecto, tendríamos que hacer algo así:

(int i, string j) = (default, default);

De lo que se trata es de que si intentamos deconstruir un default nos devuelva los valores por defecto de la deconstrucción:

(int i, string j) = default;

Una funcionalidad que si no es de las más destacadas puede ayudar a mejorar nuestro código, aunque lo de deconstruir default sea un poco creepy.

Valoración:

Useful = 6

Crazy = 3

List patterns

Esta nueva característica creo que traerá mucho que hablar en el futuro, aunque no tengo claro que puedan implementarla para esta versión de C#. Se trata de patrones para listas. Algo como esto:

collection switch
{
   { 1, 2, 3, .., 5 } => /*...*/
   { 1, .., 3, 4, 5 } => /*...*/
}

La idea es que si una lista coincide con alguno de estos patrones haga match. Una funcionalidad que podría ayudar, aunque no se me ocurre en qué circunstancias. Lo que sí que está claro es que entra pegando muy fuerte en el ranking de locuras.

Valoración:

Useful = 4

Crazy = 8

Property-Scoped Fields

Y llegamos al momento de la vagancia extrema. Cuando ya no nos apetece ni programar campos privados en nuestras clases para almacenar los valores de las propiedades. O cómo usar con un syntax-sugar el código generado en la sombra por otro syntax-sugar más antiguo. La palabra clave field dentro de definiciones de propiedades:

public string MyProperty
{
  get { return field; }
  set
  {
    field = value;
    NotifyOfPropertyChange(nameof(MyProperty));
  }
}

Esta keyword viene a sustituir a esa variable privada que creamos para almacenar el valor de una propiedad. De esta forma ya no tenemos que usar el generador de código del IDE para crearla, lo hace el propio compilador.

Admiro muchísimo las pocas ganas de escribir código que implica esta característica. Creo que soluciona el problema de tener que pensar cómo hemos llamado a un campo, o la molestia de tener que hacer dos veces clic con el ratón en la bombilla amarilla de Visual Studio.

Lo mejor de esta funcionalidad será ver un field = value todo en azul :).

Valoración:

Useful = 6

Crazy = 7

Required Properties

Y para terminar el repaso a las posibles nuevas características, otra palabra clave nueva: required. Esta palabra clave vendría a aparecer como prefijo del set de las propiedades:

class Foobar
{
  public string Foo { get; required set; }
  public string Bar { get; required init; }
}

Esto sería el equivalente de:

class Foobar
{
  public Foobar(string Foo, string Bar)
  {
    this.Foo = Foo;
    this.Bar = Bar;
  }
  public string Foo { get; set; }
  public string Bar { get; init; }
}

Algo parecido a un record:

public record Foobar(string Foo, string Bar);

Que con la nueva keyword sería una especie de clase así definida:

public class Foobar
{
  public string Foo { get; required init; }
  public string Bar { get; required init; }
}

El factor diferencial es que podemos declarar un constructor por defecto con una propiedad que no sea de solo lectura. Lo que daría un poco más de versatilidad a este tipo de nomenclatura, pero escondería algo muy importante: el constructor principal de nuestro objeto. Un caramelo envenenado en toda regla.

Valoración:

Useful = 5

Crazy = 8

Conclusiones

El año 2021 se antoja como un año magnifico que posiblemente pase inadvertido. Donde puede aparecer Visual Studio 2022. Donde se pueden cumplir las promesas de en 2020 con .Net 5, gracias a .Net 6. Y donde C# 10 puede evolucionar en la dirección de ayudar a los programadores a escribir menos código morralla y que nuestros desarrollos queden más limpios y claros.

Parece ser que esto se puede conseguir a base de nuevas características que, lejos de aportar algo realmente nuevo, nos van a permitir hacer lo mismo con muchas menos líneas.

Un año muy prolífico en features, con muchas y aparentemente muy buenas:

resultados escala sexy-loca

Si estás interesado en saber cómo sigue la evolución de la próxima versión de C#, no dudes en pasarte por la página donde se informa del estado del compilador Roslyn o el board que han preparado los miembros del proyecto del lenguaje C#.

Esperemos que no nos salgan muchas caries con tanto syntax-sugar.

buy me a beer