Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 15 de noviembre de 2022
C#

La llegada de C#11 viene acompañada de un buen número de novedades, algunas de ellas bastante jugosas, como los raw string literals o el nuevo modificador "file" que ya hemos ido viendo por aquí.

En esta ocasión, vamos a echar el vistazo a otra de las novedades que pueden resultar de bastante utilidad en nuestro día a día: los miembros requeridos.

C#11 ha introducido el modificador required, aplicable a campos y propiedades de clases y estructuras, para indicar que éstos deben ser obligatoriamente inicializados durante la construcción del objeto. Un ejemplo sencillo de uso sería el siguiente, donde creamos una clase con una propiedad requerida y otra que no lo es:

public class Friend
{
    public required string Name { get; set; }
    public string Email { get; set; }
}

Dada esta definición, el siguiente código fallará en compilación:

var friend = new Friend(); // Error CS9035: Required member 'Friend.Name'
                           // must be set in the object initializer or 
                           // attribute constructor.

A la vista del error, lo que el compilador espera es que inicialicemos la propiedad obligatoria Name. Por tanto, si lo hacemos durante la misma construcción del objeto, el código compilará sin problemas:

var friend = new Friend { Name = "John" };

Un aspecto importante a tener en cuenta es que el modificador required sólo indica que la propiedad o campo deben ser inicializados, pero el valor asignado podría ser null si el tipo de datos así permite. Por ejemplo, el siguiente código sería válido:

var peter = new Friend { Name = "Peter" };  // No compila, falta inicializar "Surname"
var john = new Friend { Name = "John", Surname = null }; // Compila bien

public class Friend
{
    public required string Name { get; set; }
    public required string? Surname { get; set; }
    public string? Email { get; set; }
}

Visto esto, sería de esperar que un código como el siguiente compilase, pues estamos definiendo un único constructor donde se inicializa el miembro requerido, sin embargo no es así:

var friend = new Friend("John"); // Error CS9035: Required member 'Friend.Name'
                                 // must be set in the object initializer or 
                                 // attribute constructor.
public class Friend
{
    public required string Name { get; set; }
    public string? Email { get; set; }

    public Friend(string name)
    {
        Name = name;
    }
}

Cuando un tipo de datos tiene miembros required, es responsabilidad del código cliente (es decir, el que construye el objeto) inicializar todos estos miembros, como en los ejemplos que hemos visto antes. Esto difiere de la forma en que se hacía tradicionalmente, donde es el autor de la clase el responsable de realizar esta inicialización.

Esto es también así cuando el código cliente utilice un constructor; observad por ejemplo el siguiente código:

var peter = new Friend("peter@server.com") { Name = "Peter", Age = 34 }; // Compila Ok
var john = new Friend("john@server.com") { Name = "John" }; // Error, falta "Age"

public class Friend
{
    public required string Name { get; set; }
    public required int Age { get; set; }
    public string? Email { get; set; }
    public Friend(string email)
    {
        Email = email;
    }
}

El establecimiento de la variable peter compila bien porque, aunque hemos llamado al constructor, hemos inicializado todos los miembros requeridos; sin embargo, la compilación fallará con la variable john porque no los hemos inicializado todos.

Así a priori esto tiene bastante sentido, pero fijaos ahora en el siguiente ejemplo, que fallará en compilación:

var john = new Friend("John", 34); // No compila

public class Friend
{
    public required string Name { get; set; }
    public required int Age { get; set; }
    public Friend(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

En este caso, aunque el constructor está inicializando los miembros requeridos y debería ser válido, no lo es por lo que comentábamos antes: es responsabilidad del código cliente inicializar estos miembros. Para hacer que el compilador sea más flexible con esto, podemos usar el atributo [SetsRequiredMembers] sobre un constructor cuando queramos indicar explícitamente que éste inicializa todos los miembros requeridos, liberando al cliente de esta responsabilidad.

var john = new Friend("John", 34); // Ok
public class Friend
{
    public required string Name { get; set; }
    public required int Age { get; set; }

    [SetsRequiredMembers]
    public Friend(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

Es importante tener en cuenta que este atributo lo único que hace es relajar las comprobaciones del compilador, haciéndole pensar que en el interior del constructor se establecen todos los miembros requeridos, pero no se realiza ninguna comprobación de que sea así. Es decir, el siguiente código compilaría, aunque dejaría la puerta abierta a la creación de objetos con un estado inválido o inconsistente:

var john = new Friend(); // Compila ok, el constructor se encarga de 
                         // inicializar los miembros requeridos

public class Friend
{
    public required string Name { get; set; }
    public required int Age { get; set; }

    [SetsRequiredMembers]
    public Friend() 
    { 
        // Aquí no inicializamos nada!
    }
}

Dado que permite saltarse la restricción required de las propiedades, [SetRequiredMembers] debe utilizarse con precaución, pues podría dar lugar a instancias de objetos con valores no válidos.

Otra forma de saltarse estas restricciones required es mediante los mecanismos de introspección de .NET (reflection) o las herramientas que tenemos disponibles en tiempo de ejecución para crear instancias al vuelo. Por ejemplo, el siguiente código muestra un par de opciones que compilarán bien, aunque darán lugar a instancias inconsistentes:

var john = new Friend(); // No compila, falta inicializar "Name" y "Age"
var friend1 = Activator.CreateInstance<Friend>(); // Compila Ok
var friend2 = Assembly
                .GetExecutingAssembly()
                .CreateInstance(typeof(Friend).FullName)
                as Friend; // Compila Ok

public class Friend
{
    public required string Name { get; set; }
    public required int Age { get; set; }
}

Bueno, creo que con lo que hemos visto ya podemos hacernos una idea del funcionamiento, al menos a alto nivel, de esta nueva característica.

Personalmente, la veo interesante a la hora de crear nuestros tipos de datos y creo que puede tener bastante aplicación, tanto a la hora de crear clases o estructuras nuevas como para añadir miembros a tipos existentes. Aunque es algo que podríamos conseguir con versiones anteriores utilizando constructores, esta fórmula es probablemente más sencilla en la mayoría de las ocasiones.

Eso sí, me quedo con la sensación de que el uso del [SetsRequiredMembers] quizás podría haberse evitado en algunos casos utilizando un poco más de magia en tiempo de compilación, pero bueno, entiendo también que esto puede ser simplemente el inicio y en el futuro quizás pueda mejorarse un poco.

Por si os interesa profundizar más, aquí tenéis algunos enlaces:

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!