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 ;)

19 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, 1 de julio de 2025
El compilador de C# vestido como un adivino que recibe una carta con el texto MemberNotNullWhen

El compilador de C# es listo, muy listo. Es capaz de analizar en tiempo real nuestro código para comprender lo que estamos haciendo y su contexto y, en muchas ocasiones, advertirnos de posibles errores antes de que seamos conscientes de que están ahí.

Pero lamentablemente aún no es adivino, y en algunas ocasiones tenemos que ayudarlo a que conozca nuestras intenciones. Para estos casos, existen una serie de atributos que añaden metadatos al código y que el compilador puede utilizar para realizar un análisis más preciso.

Hoy vamos a hablar de uno de estos atributos, MemberNotNullWhenAttribute, que nos permite dar pistas al compilador sobre la nulabilidad de un miembro de una clase en función del valor de otro miembro, algo que puede resultarnos muy útil en ciertos escenarios, como veremos a continuación.

Escenario de partida: el patrón Result

Imaginemos que estamos trabajando con un patrón de diseño que se ha vuelto muy popular en los últimos años, el patrón Result. Aunque (creo que) no está formalmente definido, esta práctica consiste en devolver un objeto que encapsula el resultado de una operación junto con información adicional, como un mensaje de error si la operación ha fallado o un valor en caso de que haya tenido éxito.

El patrón Result tiene muchos defensores que argumentan que mejora la flexibilidad, legibilidad y la robustez del código. Pero también tiene detractores, que argumentan que puede llevar a un código más complicado y difícil de mantener. Sin embargo, no vamos a entrar en este debate hoy; nuestro objetivo será simplemente usarlo como un buen ejemplo para ilustrar el uso de MemberNotNullWhenAttribute.

Una implementación super simplificada de este patrón podría ser algo así:

public class Result<T>
{
    public bool Success { get; init; }
    public string? Error { get; init; }
    public T? Value { get; init; }

    // Otro código: constructores, factorías, etc
}

Nota: normalmente las implementaciones de este patrón son algo más complejas, pero para los propósitos de este artículo, el código anterior es suficiente. Podéis ver implementaciones más completas en esta serie de Andrew Lock.

Como se puede observar, se define una propiedad Success que indica si la operación ha tenido éxito o no, y dos propiedades adicionales, ErrorText y Value, que contendrán información adicional en función del valor de Success. Si todo fue bien, Value contendrá el resultado de la operación, y si no, ErrorText contendrá un mensaje de error.

El problema es que, desde el punto de vista del compilador, esta lógica no es inferible. Tanto la propiedad ErrorText como la propiedad Value pueden ser null en cualquier momento. El compilador no puede saber cuándo no lo son y, por tanto, las ayudas a la hora de detectar posibles errores por referencias nulas en nuestro código no serán muy certeras.

Esto se puede ver fácilmente si intentamos utilizar las propiedades ErrorText y Value de un valor de tipo Result<T>, incluso habiendo comprobado previamente que Success es true o false, como en el siguiente uso de la clase que hemos visto antes:

using static System.Console;

var result1 = GetKnownFriend();
if (result1.Success)
    WriteLine(result1.Value.Name); // WARNING: Dereference of a possibly null reference

var result2 = GetUnknownFriend();
if (!result2.Success)
    WriteLine(result2.Error.Trim()); // WARNING: Dereference of a possibly null reference

Result<Friend> GetKnownFriend() => new() { Success = true, Value = new Friend() };
Result<Friend> GetUnknownFriend() => new() { Success = false, Error = "Not found" };

Fijaos en las líneas que he comentado. El compilador lanzará estos warnings para avisarnos de que estamos accediendo a propiedades que puede ser null porque están definidas respectivamente como T? y string?. No tiene forma de saber que en función del valor de Success podemos estar seguros de que no lo son.

Dando pistas al compilador con MemberNotNullWhenAttribute

Para solucionar este problema, podemos utilizar el atributo MemberNotNullWhenAttribute. Este atributo, definido en el espacio de nombres System.Diagnostics.CodeAnalysis, nos permite indicar al compilador que un miembro de una clase no será null si otro miembro de la misma clase tiene un valor bool concreto.

Este atributo se aplica sobre el miembro booleano cuyo valor condiciona la nulabilidad del otro miembro. En nuestro caso, lo aplicaremos sobre la propiedad Success de la clase Result<T>, indicando que si Success es true, entonces Value no será null, y si Success es false, entonces Error no será null.

Como podemos ver en el siguiente código, el atributo MemberNotNullWhenAttribute acepta dos parámetros. En el primero, indicaremos el valor (true o false) que debe tener el miembro booleano para que el miembro cuyo nombre pasamos como segundo parámetro no sea null:

public class Result<T>
{
    [MemberNotNullWhen(true, nameof(Value))]  // Si Success es true, Value no es null
    [MemberNotNullWhen(false, nameof(Error))] // Si Success es false, Error no es null
    public bool Success { get; init; }

    public string? Error { get; init; }
    public T? Value { get; init; }

    // Constructores, factorías, etc
}

Con esto, el compilador ya tiene información suficiente para inferir cuándo una propiedad puede contener un nulo, a pesar de que su tipo lo permita y afinará más a la hora de mostrar errores relacionados con la nulabilidad:


var result1 = GetKnownFriend();
if (result1.Success)
    WriteLine(result1.Value.Name); // Ya no hay warning, pues Success es true

var result2 = GetUnknownFriend();
if (!result2.Success)
    WriteLine(result2.Error.Trim()); // Ya no hay warnings, porque Success es false

¿Y este es el único atributo de este tipo que existe?

Pues no, de hecho hay otros atributos para las comprobaciones estáticas de valores nulos que son interpretados por el compilador para mejorar las advertencias, y que podéis consultar en la documentación oficial. Algunos de ellos son:

  • AllowNull
  • DisallowNull
  • MaybeNull
  • NotNull
  • MaybeNullWhen
  • NotNullWhen
  • NotNullIfNotNull
  • MemberNotNull

En artículos posteriores quizás echaremos el vistazo a algunos de ellos, porque nos pueden ser de utilidad 🙂

¡Espero que os haya resultado interesante!

Publicado en VariableNotFound.

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