
Hace algún tiempo hablamos sobre el atributo [MemberNotNullWhen]
y vimos cómo se podía usar para mejorar la seguridad frente a valores nulos en nuestras aplicaciones.
Como recordaréis, este atributo lo aplicábamos sobre miembros booleanos para asegurar que otro miembro de su misma clase no contendrá null
, dependiendo del valor (true
o false
) del primero. El ejemplo que lo ilustraba era el siguiente:
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
}
Pero también comentamos que existían muchos otros atributos que el compilador empleaba para afinar en la detección de posibles problemas similares en el código. Al usarlos, el compilador comprende mejor las intenciones de nuestro código respecto a la utilización de nulos, y puede generar advertencias que impiden posteriores errores en tiempo de ejecución, pero sólo cuando son estrictamente necesarias.
En este nuevo post, vamos a echar un vistazo a los siguientes:
[NotNullWhen]
: el parámetro out
no será nulo cuando el método devuelva un valor específico
Este atributo se aplica a parámetros de salida (out
) de un método booleano, y se utiliza para indicar que el valor del parámetro no será nulo dependiendo del valor de retorno del método.
Un ejemplo típico lo encontramos con métodos como TryXXX()
, que devuelven un valor booleano que indica si la operación fue exitosa o no, y disponen de un parámetro out
que contiene el resultado de la operación. En este caso, el atributo se aplicaría al parámetro out
para indicar que su valor no será nulo si el método devuelve true
.
Veamos un ejemplo sencillo. El siguiente método TryGetFriend()
intenta obtener un amigo a partir de su identificador. Si el amigo existe, devuelve true
y el parámetro out
contendrá el objeto Friend
. Si no existe, devuelve false
y el parámetro out
será nulo.
bool TryGetFriend(int id, [NotNullWhen(true)] out Friend? value)
{
if (id == 1)
{
value = new Friend("John", 30);
return true;
}
value = null;
return false;
}
record Friend(string Name, int Age);
Ahora, si intentamos usar el método TryGetFriend()
de la siguiente manera, veremos que el compilador nos advierte de que el valor de value
puede ser nulo, y nos sugiere usar una declaración if
para comprobarlo, o un acceso condicional (?.
) para evitar un NullReferenceException
:
if (TryGetFriend(1, out var friend))
{
Console.WriteLine(friend.Name); // CS8602: Dereference of a possibly null reference
}
Es obvio; sin darle más pistas, el compilador no puede saber si friend
contendrá un valor nulo en ese punto, porque desconoce que el hecho de que la llamada al método retorne true
implica que el valor de friend
no será nulo.
Sin embargo, si aplicamos el atributo [NotNullWhen(true)]
al parámetro out
, el compilador ya tendrá la certeza de que el valor de friend
no será nulo si la llamada al método devuelve true
, y no generará la advertencia:
bool TryGetFriend(int id, [NotNullWhen(true)] out Friend? value)
{
...
}
[NotNullIfNotNull]
: el método no devolverá un valor nulo si el parámetro indicado no lo es
Este atributo se aplica sobre el retorno de un método, indicando que éste no será nulo cuando el parámetro cuyo nombre especificamos tampoco lo sea. Por ejemplo, observad el siguiente método ToUpper()
que convierte una cadena a mayúsculas:
string? ToUpper(string? inputString)
{
return inputString?.ToUpper();
}
El método devolverá un valor nulo si se le envía un nulo, pero retornará una cadena válida en caso contrario. Sin embargo, por su firma, el compilador no puede determinar la relación entre la nulabilidad del parámetro entrante y el valor de retorno. Por lo tanto, si usamos el método de la siguiente manera obtendremos la advertencia de posible acceso a una referencia nula:
var str = ToUpper("Hello world");
Console.WriteLine(str.Length); // CS8602: Dereference of a possibly null reference
Apliquemos el atributo [NotNullIfNotNull]
al retorno del método de la siguiente manera:
[return:NotNullIfNotNull(nameof(inputString))]
string? ToUpper2(string? inputString)
{
return inputString?.ToUpper();
}
Hecho ese cambio, el compilador ya sabrá que el valor de retorno no será nulo si el parámetro tampoco lo es, y generará la advertencia sólo cuando sea estrictamente necesaria:
var str1 = ToUpper("Hello world");
Console.WriteLine(str1.Length); // Ok, sin advertencias
var str2 = ToUpper(null);
Console.WriteLine(str2.Length); // CS8602: Dereference of a possibly null reference
[MemberNotNull]
: el miembro no será nulo después de ejecutar un código
Con este atributo podemos indicar los miembros de una clase que no serán nulos después de invocar un método o acceder a una propiedad.
Observad el siguiente ejemplo. La clase Friend
tiene una propiedad Name
que puede ser nula, y un método Initialize()
que la inicializa. Sin embargo, el compilador no puede saber que después de llamar a Initialize()
, la propiedad Name
no será nula.
Por tanto, ante la duda, el compilador nos advierte de que la propiedad Name
podría ser nula:
var f = new Friend();
Console.WriteLine(f.Name.Length); // CS8602: Dereference of a possibly null reference
public class Friend
{
public string? Name { get; set; }
public void Initialize()
{
Name = "Anonymous";
}
}
Para indicarle al compilador que la propiedad Name
no será nula después de ejecutar el método Initialize()
, aplicamos el atributo [MemberNotNull]
a la propiedad:
public class Friend
{
public string? Name { get; set; }
[MemberNotNull(nameof(Name))]
public void Initialize()
{
Name = "Anonymous";
}
}
De esta forma, tras la llamada a Initialize()
, el compilador no generará la advertencia:
var f = new Friend();
// Todavía no podemos asegurar que Name no sea nulo:
Console.WriteLine(f.Name.Length); // CS8602: Dereference of a possibly null reference
f.Initialize();
Console.WriteLine(f.Name.Length); // Ok, porque seguro que Name no es nulo
Por último, es importante destacar que el atributo [MemberNotNull]
no se puede utilizar en constructores, y permite especificar varios miembros separados por comas, como en el siguiente ejemplo:
[MemberNotNull(nameof(Name), nameof(Address))]
Publicado en Variable not found.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario