Inicialización diferida en C #

La inicialización perezosa o inicialización "perezosa" es una forma de acceder a un objeto, ocultando detrás de sí mismo un mecanismo que le permite diferir la creación de este objeto hasta el momento en que se accede por primera vez. La necesidad de una inicialización diferida puede surgir por varias razones: desde el deseo de reducir la carga en el inicio de la aplicación hasta la optimización de la funcionalidad que rara vez se usa. De hecho, no siempre se utilizan todas las funciones de la aplicación y, además, de forma inmediata, por lo tanto, es bastante racional posponer la creación de objetos que las implementen hasta tiempos mejores. Me gustaría revisar las opciones de inicialización diferida disponibles en el lenguaje C #.



Para demostrar ejemplos, usaré la clase Test, que tiene una propiedad BlobData que devuelve un objeto de tipo Blob, que, según la leyenda, se crea con bastante lentitud y se decidió crearlo de forma perezosa.



class Test
{
    public Blob BlobData
    {
        get
        {
            return new Blob();
        }
    }
}




Comprobando nulo



La opción más simple, disponible desde las primeras versiones del lenguaje, es crear una variable no inicializada y probarla para nula antes de regresar. Si la variable es nula, cree un objeto y asígnelo a esta variable, y luego devuélvalo. En el acceso repetido, el objeto ya estará creado y lo devolveremos inmediatamente.



class Test
{
    private Blob _blob = null;

    public Blob BlobData
    {
        get
        {
            if (_blob == null)
            {
                _blob = new Blob();
            }

            return _blob;
        }
    }
}




Aquí se crea un objeto de tipo Blob cuando se accede por primera vez a la propiedad. O no se crea, si por alguna razón el programa no lo necesitaba en esta sesión.



Operador ternario ?:



C # tiene un operador ternario que le permite probar una condición y, si es verdadera, devolver un valor y, si es falso, otro. Podemos usarlo para acortar y simplificar un poco el código.



class Test
{
    private Blob _blob = null;

    public Blob BlobData
    {
        get
        {
            return _blob == null
                ? _blob = new Blob()
                : _blob;
        }
    }
}




La esencia sigue siendo la misma. Si el objeto no está inicializado, inicialícelo y vuelva. Si ya está inicializado, lo devolvemos inmediatamente.



es nulo



Las situaciones son diferentes y, por ejemplo, podemos encontrar una en la que la clase Blob tenga un operador == sobrecargado. Para hacer esto, probablemente necesitemos hacer la verificación es nulo en lugar de == nulo. Disponible en las últimas versiones del idioma.



return _blob is null
    ? _blob = new Blob()
    : _blob;




Pero esto es así, una pequeña digresión.



¿El operador de coalescencia nula?



El operador binario nos ayudará a simplificar aún más el código.

La esencia de su trabajo es la siguiente. Si el primer operando no es nulo, se devuelve. Si el primer operando es nulo, se devuelve el segundo.



class Test
{
    private Blob _blob = null;

    public Blob BlobData
    {
        get
        {
            return _blob ?? (_blob = new Blob());
        }
    }
}




El segundo operando tuvo que estar entre paréntesis debido a la prioridad de las operaciones.



Operador ?? =



C # 8 introduce un operador de asignación de fusión nula que se ve así ?? =

Cómo funciona es el siguiente. Si el primer operando no es nulo, simplemente se devuelve. Si el primer operando es nulo, se le asigna el valor del segundo y se devuelve este valor.



class Test
{
    private Blob _blob = null;

    public Blob BlobData
    {
        get
        {
            return _blob ??= new Blob();
        }
    }
}




Esto nos permitió reducir un poco más el código.



Corrientes



Si existe la posibilidad de que varios subprocesos puedan acceder a un recurso determinado a la vez, deberíamos hacerlo seguro para subprocesos. De lo contrario, puede darse una situación en la que, por ejemplo, ambos subprocesos comprobarán el objeto en busca de nulo, el resultado será falso y luego se crearán dos objetos Blob, cargando el sistema el doble de lo que quisiéramos, y además, uno de estos los objetos se guardarán y el segundo se perderá.



class Test
{
    private readonly object _lock = new object();
    private Blob _blob = null;

    public Blob BlobData
    {
        get
        {
            lock (_lock)
            {
                return _blob ?? (_blob = new Blob());
            }
        }
    }
}




La instrucción de bloqueo adquiere un bloqueo mutuamente excluyente en el objeto especificado antes de ejecutar ciertas instrucciones y luego libera el bloqueo. Es equivalente a usar System.Threading.Monitor.Enter (..., ...);



Perezoso <T>



.NET 4.0 introdujo la clase Lazy para ocultar todo este trabajo sucio de nuestros ojos. Ahora solo podemos dejar una variable local de tipo Lazy. Al acceder a su propiedad Value, obtenemos un objeto de la clase Blob. Si el objeto se creó antes, volverá inmediatamente; de ​​lo contrario, se creará primero.



class Test
{
    private readonly Lazy<Blob> _lazy = new Lazy<Blob>();

    public Blob BlobData
    {
        get
        {
            return _lazy.Value;
        }
    }
}




Dado que Blob tiene un constructor sin parámetros, Lazy puede crearlo en el momento adecuado, sin hacer preguntas. Si necesitamos realizar algunas acciones adicionales durante la creación del objeto Blob, el constructor de la clase Lazy puede tomar una referencia a Func <T>



private Lazy<Blob> _lazy = new Lazy<Blob>(() => new Blob());




Además, en el segundo parámetro del constructor, podemos indicar si necesitamos seguridad de subprocesos (el mismo bloqueo).



Propiedad



Ahora acortemos la notación de la propiedad readonly, ya que el C # moderno le permite hacer esto muy bien. Al final, todo se ve así:



class Test
{
    private readonly Lazy<Blob> _lazy = new Lazy<Blob>();
    public Blob BlobData => _lazy.Value;
}




LazyInitializer



También hay una opción para no envolver la clase en un contenedor Lazy, sino utilizar la funcionalidad LazyInitializer. Esta clase tiene un método estático AsegureInitialized con un montón de sobrecargas que le permiten crear cualquier cosa, incluida la seguridad de subprocesos y escribir código personalizado para crear un objeto, pero cuya esencia principal es la siguiente. Compruebe si el objeto no está inicializado. Si no es así, inicialice. Devuelve un objeto. Usando esta clase, podemos reescribir nuestro código así:



class Test
{
    private Blob _blob;
    public Blob BlobData => LazyInitializer.EnsureInitialized(ref _blob);
}




Eso es todo. Gracias por su atención.



All Articles