Autor en Google+
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 ;)

15 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, 23 de noviembre de 2021
.NET

Estamos muy acostumbrados a comenzar nuestros métodos realizando comprobaciones para evitar que pasen a nuestro código valores nulos que pudieran romper la aplicación.

Desde el principio de los tiempos, estas guardas han presentado el siguiente aspecto:

public class MyService
{
    public void MyMethod(object first, object second)
    {
        if(first == null) 
        {
            throw new ArgumentNullException("first");
        }
        if(second == null) 
        {
            throw new ArgumentNullException("second");
        }        
        // ...
    }
}    

¿Todo bien, verdad? El código es aparentemente correcto y fácil de comprender, pero... ¡demasiado extenso! Hemos consumido casi diez líneas de código sólo realizando comprobaciones de "fontanería", y ni siquiera hemos empezado a plantear la funcionalidad real del método.

Afortunadamente, con el tiempo C# ha ido evolucionando y mejorando sucesivamente este escenario tan frecuente.

C# 6: el operador nameof

Aunque no iba en la línea de simplificar el código, el operador nameof, introducido en C# 6, permitió solucionar un pequeño problema que tenían esas comprobaciones, potencial causante de problemas o confusiones. Estábamos creando una sutil y frágil dependencia entre el nombre del parámetro a nivel de código y la cadena que estamos enviando al constructor de la excepción ArgumentNullException:

if(first == null) 
{
    throw new ArgumentNullException("first");
}

Esto se puede entender rápidamente si refactorizamos el método para renombrar el parámetro first a one. El código resultante sería el siguiente, donde podemos ver que el valor enviado al instanciar la excepción no se corresponde con el nombre real del parámetro, pues al tratarse de una cadena de texto no se ha visto afectada por el renombrado:

public void MyMethod(object one, object second)
{
    if(one == null) 
    {
        throw new ArgumentNullException("first"); // Debería ser "one"!
    }
    ...
}

Si bien no es un error, ciertamente podría despistar al ver la excepción, pues haría referencia a un parámetro inexistente (o, en el peor de los casos, a un parámetro incorrecto):

Unhandled exception. System.ArgumentNullException: Value cannot be null. Parameter name: first

El operador nameof permite obtener en tiempo de ejecución el nombre asignado a nivel de código fuente a cualquier tipo de elemento (variables, parámetros, propiedades, métodos, etc.), de forma que evitamos estas dependencias:

public class MyService
{
    public void MyMethod(object first, object second)
    {
        if(first == null) 
        {
            throw new ArgumentNullException(nameof(first));
        }
        if(second == null) 
        {
            throw new ArgumentNullException(nameof(second));
        }        
        // ...
    }
}

Bueno, hemos mejorado en algo, pero seguimos teniendo que escribir demasiado, ¿verdad? Por suerte, C# 7 no tardó en llegar :)

C# 7: expresiones throw

El compilador de C# 7 comenzó a mirar las instrucciones throw con otros ojos, tratándolas a todos los efectos como si fueran expresiones, gracias a la introducción de las throw expressions.

Recordemos que, en la práctica, esta característica permite introducir un throw en cualquier punto del código donde pueda ir una expresión, como en el siguiente ejemplo:

public static int Divide(int a, int b)
{
    return a / (b != 0 ? b: throw new ArgumentOutOfRangeException(nameof(b)));
}

Pues bien, uniendo esto al null coalescing operator (??) introducido años atrás, en los tiempos de C# 2, permite simplificar considerablemente el código de comprobación de nulos:

public class MyService
{
    public void MyMethod(object first, object second)
    {
        first = first ?? throw new ArgumentNullException(nameof(first));
        second = second ?? throw new ArgumentNullException(nameof(second));
        // ...
    }
}

Vamos mejorando, pero aún el lenguaje podía darnos algo más...

C# 8: null coalescing assignment

No soy capaz de traducir el nombre de esta característica, pero intentaré explicar para qué sirve 😁. Suponed un código como el siguiente, que seguro que habéis implementado más de una vez, en el que asignamos una variable sólo si ésta contiene un nulo:

// Usando un if:
if(myObject == null)
{
    myObject = new MyClass();
}

// O bien, usando el operador "??":
myObject = myObject ?? new MyClass();

El null coalescing operator permite asignar una variable cuando ésta sea null en un único paso:

myObject ??= new MyClass();

Ahora, si recordamos el punto en el que C# 7 dejó nuestro código de comprobación de argumentos nulos, vemos que la idea puede encajar bastante y ayudarnos a simplificarlo aún más:

public class MyService
{
    public void MyMethod(object first, object second)
    {
        first ??= throw new ArgumentNullException(nameof(first));
        second ??= throw new ArgumentNullException(nameof(second));
        // ...
    }
}

Parece difícilmente mejorable... ¿o no?

.NET 6: empoderando ArgumentNullException

Con .NET 6/C# 10 nos llega un interesante método estático en la clase ArgumentNullException que simplifica aún más el código necesario para lanzar esa excepción si el valor recibido es nulo.

public class MyService
{
    public void MyMethod(object first, object second)
    {
        ArgumentNullException.ThrowIfNull(first);
        ArgumentNullException.ThrowIfNull(second);
        // ...
    }
}

Imposible de mejorar, ¿verdad? Después veremos que no ;)

En cualquier caso, esta mejora es posible gracias al uso de otra novedad de C# 10, el atributo [CallerArgumentExpression]. A esto dedicaremos un post en el futuro, pero de momento basta con saber que es un atributo al estilo de [CallerMemberName], [CallerFilePath] y [CallerLineNumber], introducidos en C# 5, cuyo objetivo es obtener información sobre el consumidor de un método.

Podemos ver su utilización en el código fuente de ArgumentNullException, e intuir cómo funciona:

public class ArgumentNullException : ArgumentException
{
    ...
    public static void ThrowIfNull(
             [NotNull] object? argument, 
             [CallerArgumentExpression("argument")] string? paramName = null)
    {
        ...
    }
}

Si realizamos una invocación como ArgumentNullException.ThrowIfNull(first), el valor recibido en argument será el valor de first, mientras que en paramName recibiremos la cadena "first". Esto permitirá mostrar el error exactamente con la expresión utilizada en el punto de la llamada.

.NET 6 / C# 10: doble salto mortal con los global using static

Pues sí, aunque suene un poco a feature abuse, podemos darle una vuelta más al asunto haciendo uso de las directivas globales. Aunque en un post ya vimos cómo usar usings globales para importar espacios de nombres, también podemos utilizarla para facilitar el acceso a miembros estáticos.

En la práctica, podemos añadir el cualquier archivo del proyecto una directiva como la siguiente (aunque también podemos hacerlo en el .csproj):

global using static System.ArgumentNullException

Y de esta forma, podemos reducir aún más nuestro código de partida:

public class MyService
{
    public void MyMethod(object first, object second)
    {
        ThrowIfNull(first);
        ThrowIfNull(second);
        // ...
    }
}

Esto ya sí que no lo mejora nadie. ¿O sí?

C# vNext: simplificando el chequeo de argumentos nulos

Se está cociendo una nueva mejora en el lenguaje que probablemente será el golpe definitivo al código de chequeo de nulos al inicio de nuestros métodos.

Aunque de momento está en desarrollo y las cosas podrían cambiar, las comprobaciones simplificadas de argumentos nulos o Simplified Null Argument Checking, nos permitirán eliminar totalmente estas comprobaciones.

En su lugar, simplemente definiremos los parámetros que queremos comprobar usando una exclamación, de la siguiente forma:

public class MyService
{
    public void MyMethod(object first!!, object second!!)
    {
        // ...
    }
}

¡Voila! El único hecho de añadir esas exclamaciones al nombre del parámetro hará que en tiempo de ejecución se comprueben los valores, y se lance de forma automática una excepción ArgumentNullException cuando el valor sea nulo.

Fijaos que las exclamaciones están en el parámetro, no en su tipo: esto es así porque se trata de que el sistema añada una comprobación en tiempo de ejecución, pero no afecta al tipo del parámetro.

Punto extra: recordad que usar "==" no es la mejor forma de comprobar los nulos

Si aún con estas novedades del lenguaje y la plataforma preferís utilizar la clásica sentencia if para comprobar los nulos, es interesante que tengáis en cuenta dos cosas.

En primer lugar, ya hablamos hace tiempo de usar el operador "==" para comparar con valores nulos no era la forma más correcta de hacerlo, pues dicho operador podía ser sobrecargado para otorgarle un comportamiento distinto del esperado.

De hecho, la forma recomendable de comprobar la nulidad de un objeto es utilizar el operador is:

if(myObj is null)
{
    ...
}

De la misma forma, si queremos comprobar justamente lo contrario, es decir, que un objeto no sea nulo, podemos aprovechar los patrones combinacionales introducidos por C# 9 y hacerlo de esta forma tan elegante:

if(myObj is not null)
{
    ...
}

¡Y esto es todo! Espero que este recorrido por las diferentes opciones que tenemos y tendremos para evitar la entrada de argumentos nulos en los métodos os haya resultado interesante :)

Publicado en Variable not found.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

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

Artículos relacionados: