DEV Community

C# { get; init; }

C# Logo

Azúcar sintáctico

Lo cierto es que C# se encuentra en un estado bastante lejos de una estabilidad, lo que se esperaría despues de casi 25 años (la primera distribución fue en 2000).

Mucho de estas nuevas características no son realmente más que azúcar sintáctico. El azúcar sintáctico, o también syntactic sugar, consiste en modificar el compilador para que actúe como una especie de preprocesador de nuevas características, generando código ya previamente aceptado.

Tomemos por ejemplo la siguiente clase, que representa una tarjeta bancaria:

using System;
using System.Diagnostics;
using System.Collections.Generic;


public class Tarjeta {  
    public Tarjeta(int num, DateOnly caduca)
    {
        Debug.Assert( num > 0, "num. de tarjeta debe ser positivo" );
        Debug.Assert( caduca > DateOnly.FromDateTime( DateTime.Now ), "caducidad debe ser a futuro" );

        this.Num = num;
        this.Caduca = caduca;
        this.calculaCVC();
    }

    public int Num { get; private set; }
    public DateOnly Caduca { get; private set; }
    public int CVC { get; private set; }

    private void calculaCVC()
    {
        this.CVC = this.Num % 1000;
    }

    public override string ToString()
    {
        return $"{this.Num} ({this.Caduca}, {this.CVC})";
    }
}
Enter fullscreen mode Exit fullscreen mode

Podríamos crear un objeto de esta clase con var t1 = new Tarjeta( 548901687, new DateOnly( 2034, 11, 5 ) );, y si visualizamos por consola este objeto obtendríamos 548901687 (11/5/2034, 687). Nótese que es un ejemplo en el que hemos simplificado mucho.

Esta claro cuáles son las invariantes de esta clase: el número de la tarjeta, el CVC y la caducidad, no varían una vez asignados; el CVC se calcula a partir de la numeración; y finalmente la caducidad tiene que estar en el futuro.

La nueva característica son las propiedades de inicialización (init), que pueden inicializarse en el momento de creación del objeto. Podemos marcarlas también como requeridas (required), de forma que, si no se aportan en el momento de creación del objeto, se produce un error de compilación.

Por ejemplo, la propiedad Num, la numeración de la tarjeta podría pasar a ser la siguiente:

class Tarjeta {
    public required int Num { get; init; }
    // más cosas...
}
Enter fullscreen mode Exit fullscreen mode

Así que la inicialización del objeto sería var t1 = new Tarjeta { Num = 548901687 };. Quizás se podría argumentar que la inicialización es más explícita, ya que obligatoriamente se menciona el nombre de la propiedad (al llamar al constructor se puede mencionar el nombre del argumento, pero ni es obligatorio ni el parámetro tiene que tener un nombre significativo).

Está claro que no es más que la creación de un método inicializador del objeto por parte del compilador en el que se realizan estas asignaciones.

Probablemente, si nos parece interesante, nos dediquemos con entusiasmo a portar nuestro código sobre la clase Tarjeta.

public class Tarjeta {
    public required int Num { get; init; }
    public required DateOnly Caduca { get; init; }
    public int CVC { get; private set; }

    private void calculaCVC()
    {
        this.CVC = this.Num % 1000;
    }

    public override string ToString()
    {
        return $"{this.Num} ({this.Caduca}, {this.CVC})";
    }
}
Enter fullscreen mode Exit fullscreen mode

Fantástico. Ahora podemos crear objetos de la tarjeta solo con el siguiente código:

var t1 = new Tarjeta {
    Num = 548901687,
    Caduca = new DateOnly( 2034, 11, 5 )
};

Console.WriteLine( t1 );
Enter fullscreen mode Exit fullscreen mode

Genial, ¡somos modernos!

Pero si analizamos un poco el código, veremos que, en realidad, hemos perdido la mayor parte de invariantes. Las propiedades pueden ser inmutables, pero no hay forma de comprobar que la numeración sea un entero positivo. ¡Tampoco estamos calculando el CVC!

Vale, no pasa nada. Vamos a modificar las propiedades para poder reestablecer estas invariantes.

public class Tarjeta {
    public required int Num {
        get => this._num;
        init {
            Debug.Assert( value > 0, "num. de tarjeta debe ser positivo" );
            this._num = value;
        }
    }

    public required DateOnly Caduca {
        get => this._caduca;
        init {
            Debug.Assert( value > DateOnly.FromDateTime( DateTime.Now ), "caducidad debe ser a futuro" );
            this._caduca = value;
        }
    }

    public int CVC { get; private set; }

    public Tarjeta()
    {
        this.calculaCVC();
    }

    private void calculaCVC()
    {
        this.CVC = this.Num % 1000;
    }

    public override string ToString()
    {
        return $"{this.Num} ({this.Caduca}, {this.CVC})";
    }

    private int _num;
    private DateOnly _caduca;
}
Enter fullscreen mode Exit fullscreen mode

Ahora sí. Volvemos a cumplir todos los compromisos de la clase. Creamos nuestro objeto, lo visualizamos y...

var t1 = new Tarjeta {
        Num = 548901687,
        Caduca = new DateOnly( 2034, 11, 5 )
};

Console.WriteLine( t1 ); // 548901687 (11/5/2034, 0)
Enter fullscreen mode Exit fullscreen mode

¿Un cero como CVC? La única forma de que el CVC pueda llegar a ser 0 es que a su vez Num sea también 0. Está claro que la numeración Num todavía no se ha asignado a su valor. Podemos deducir que el código generarado por el compilador para la creaciónde t1 más arriba sería algo como:

class Tarjeta {
    public Tarjeta(int num, DateOnly cadunca)
    {   
        this.calculaCVC();
                this.__init( num, caduca );
    }

    private void __init(int num, DateOnly caduca)
    {
        this.Num = num;
        this.Caduca = caduca;
    }

    public int Num { get; private set; }
    public DateOnly Caduca { get; private set; }
    public int CVC { get; private set; }

        // más cosas...
}

var t1 = new Tarjeta( num: 548901687,
                      caduca: new DateOnly( 2034, 11, 5 ) );
Enter fullscreen mode Exit fullscreen mode

Por eso se le llama azúcar sintáctico, porque no es que se aporte una sintaxis nueva soportando una nueva construcción expresiva, sino que el compilador genera código ya conocido.

La llamada al inicializador que el compilador inyecta al final del constructor, se produce por tanto después de cualquier código ejecutable en dicho constructor. No tenemos oportunidad de hacer nada con los valores a las propiedades ya asignadas.

Así que está claro que no es bueno mezclar propiedades de inicialización y constructores. O al menos, no si el código del constructor depende de los valores que se van a inicializar mediante esas propiedades.

Así que si no podemos depender del constructor, ni para calcular el CVC ni para garantizar las invariantes, tenemos que hacer cambios.

public class Tarjeta {
    public required int Num {
        get => this._num;
        init {
            Debug.Assert( value > 0, "num. de tarjeta debe ser positivo" );
            this._num = value;
                        this.calculaCVC();
        }
    }

    public required DateOnly Caduca {
        get => this._caduca;
        init {
            Debug.Assert( value > DateOnly.FromDateTime( DateTime.Now ), "caducidad debe ser a futuro" );
            this._caduca = value;
        }
    }

    public int CVC { get; private set; }

        // más cosas...

    private int _num;
    private DateOnly _caduca;
}
Enter fullscreen mode Exit fullscreen mode

Bueno, pues ahora sí que ya podemos inicializar las propiedades en lugar de llamar al constructor ("a la antigua"). ¿Merece la pena?

Top comments (0)