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, 30 de enero de 2024
C#

Hace ya algunos años, con la llegada de C# 9, los records nos mostraron otra forma de crear objetos distinta a los clásicos constructores. Ya entonces, y únicamente en aquél ámbito, se comenzó a hablar de primary constructors, pero no ha sido hasta C# 12 cuando se ha implementado de forma completa esta característica en el lenguaje.

En este post vamos a ver de qué se trata, cómo podemos utilizarlos y peculiaridades que hay que tener en cuenta.

Básicamente, los constructores primarios o principales (no sé cuál es la traducción más correcta, pero me me gusta más la primera 😉) proporcionan una sintaxis más concisa para definir clases o estructuras que necesitan un constructor para poder ser materializadas.

A diferencia de los constructores tradicionales, los parámetros de entrada se definen sobre la propia clase o estructura y estarán disponibles en todo su cuerpo sin necesidad de mapearlos a campos o propiedades internas.

Vamos a ver un ejemplo muy simple de su uso, pero partiremos de la implementación de una clase básica, que necesita 9 líneas de código si usamos la fórmula "clásica" de C#:

Console.WriteLine(new Person("John")); // Muestra "John" por consola

// Clase con constructor tradicional
public class Person
{
    private readonly string _name;
    public Person(string name)
    {
        _name = name;
    }
    public override string ToString() => _name;
}

El código equivalente usando un constructor primario sería el siguiente, donde podéis ver que lo hemos reducido a sólo 4 líneas (50%, poco más, poco menos):

public class Person(string name)
{
    public override string ToString() => name;
}

Ahora, teniendo este código en mente, fijaos en varios puntos importantes.

Primero, los parámetros del constructor primario se definen entre paréntesis después del nombre de la clase o estructura. Es decir, no es necesario definir un método constructor ni nada parecido, todo va ahí.

Segundo, no ha hecho falta guardar el valor del parámetro name en ningún sitio. Todos los miembros de la clase o estructura tienen acceso a él, como puedes comprobar en el método ToString().

Fijaos también en que en dicho método hemos accedido de forma directa al parámetro, no hemos necesitado usar this porque realmente se trata de un parámetro del constructor primario, no un campo o propiedad de la clase.

Los constructores primarios pueden convivir con otros constructores, es decir, nada impide que una clase como la anterior tenga constructores adicionales. La única pega es que estos otros constructores deberán llamar obligatoriamente al constructor primario para asegurar que los parámetros sean inicializados antes de usarlos. Podemos ver un ejemplo a continuación:

public class Person(string name, int? age)
{
   public Person(string name): this(name, 0) { }
   public Person(int age):     this("Anonymous", age) { }

   public override string ToString() => name;
}

Y por supuesto, podemos usarlos para recibir parámetros de cualquier tipo u origen. Por ejemplo, un escenario donde serán especialmente útiles es a la hora de recibir los servicios requeridos por una clase cuando usamos inyección de dependencias, pues evitaremos mucho código de fontanería:

public class InvoiceServices(IInvoice repository, IUnitOfWork uow, INotifier notifier)
{
    public async Task<Invoice> CreateInvoice(Invoice invoice) 
    {
        repository.Add(invoice);
        await uow.CommitAsync();
        await notifier.NotifyNewInvoiceAsync(invoice);
        ...
    }
}

Obviamente, los parámetros del constructor primario no estarán disponibles desde miembros estáticos porque no hay ninguna instancia por detrás cuyo constructor recoja los valores. Por tanto, el siguiente código fallaría en compilación:

public class Person(string name)
{
   public override string ToString() => name;

   // Error	CS9105:	
   // Cannot use primary constructor parameter 'string name' in this context.
    public static void Print() => Console.WriteLine(name);
}

Aunque no sean visibles, los parámetros de los constructores primarios son introducidos en campos privados, inaccesibles para el desarrollador. El compilador simplemente hace la "magia" de transformar el acceso a los parámetros del constructor en acceso a estos campos privados. Y, como tales, sus valores pueden ser modificados desde el código (algo que no se me antoja especialmente recomendable, pero bueno):

var person = new Person("John");
person.SetName("Peter");
Console.WriteLine(person); // Muestra "Peter" por consola

public class Person(string name)
{
    public void SetName(string newName) => name = newName;
    public override string ToString() => name;
}

Estos campos privados no son visibles fuera de la clase, aunque podríamos crear propiedades de acceso a ellos, como hemos hecho siempre:

public class Person(string name)
{
    public string Name => name;
    public override string ToString() => name;
}

Obviamente, estos dos últimos aspectos no aplican en records, que por definición son inmutables y los parámetros de sus constructores primarios son propiedades públicas:

var person = new Person("John");
Console.WriteLine(person.Name); // Muestra "John" por consola

public record Person(string Name)
{
    // Error CS8852: no se puede asignar Person.Name
    public void SetName(string newName) => Name = newName;
}

Conclusiones

En definitiva, los constructores primarios son una interesante mejora que, en la línea de muchas otras novedades recientes, continúan haciendo el código C# cada vez más conciso.

Aunque la sintaxis pueda resultar algo rara al principio, no creo que vayamos a tardar demasiado en acostumbrarnos a ella y comenzar a introducirla en nuestros desarrollos. Que al final, cualquier cambio destinado a que nuestras manos sufran menos, bienvenido es 😉

Publicado en: www.variablenotfound.com.

2 Comentarios:

David dijo...

Hola José, como comentas lo interesante sería también poder definir esas variables aparte de privadas que sean de solo lectura.

Ya que normalmente declaramos una variable privada readonly y en el constructor le asignamos algún parámetro inyectado por el contenedor de dependencias.

readonly IMyService MyService;

public MyConstructor(IMyService service)
{
MyService = service;
}

Saludos.

José María Aguilar dijo...

Hola,

Sin duda, sería interesante. Como escribía en el post, eso de que los parámetros sean alterables tampoco es algo que me parezca una una buena idea.

En cualquier caso, hay algunos truquillos que se pueden aplicar para conseguirlo, en breve publicaré un post sobre el tema.

Muchas gracias por comentar!