Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En un contexto habilitado que admite un valor NULL, el compilador realiza un análisis estático del código para determinar el estado NULL de todas las variables de tipo de referencia:
- not-null: el análisis estático determina que la variable tiene un valor distinto de NULL.
- maybe-null: el análisis estático no puede determinar que una variable está asignada a un valor distinto de NULL.
Estos estados permiten al compilador proporcionar advertencias cuando se podría desreferenciar un valor NULL, lo que produce un System.NullReferenceException. Estos atributos proporcionan al compilador información semántica sobre el estado NULL de argumentos, valores devueltos y miembros de objeto. Los atributos aclaran el estado de los argumentos y los valores devueltos. El compilador proporciona advertencias más precisas cuando las API se anotan correctamente con esta información semántica.
En este artículo se proporciona una breve descripción de cada uno de los atributos de tipo de referencia que acepta valores NULL y cómo usarlos.
Comencemos con un ejemplo. Imagine que la biblioteca tiene la SIGUIENTE API que recupera una cadena de recursos. Este método se compiló originalmente en un contexto de tipo "oblivious" que admite un valor NULL:
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
En el ejemplo anterior se sigue el conocido patrón de Try* en .NET. Hay dos parámetros de referencia para esta API: key y message. Esta API tiene las siguientes reglas relacionadas con el estado NULL de estos parámetros:
- Los autores de la llamada no deben pasar
nullcomo argumento parakey. - Los autores de la llamada pueden pasar una variable cuyo valor sea
nullcomo argumento demessage. - Si el método
TryGetMessagedevuelvetrue, el valor demessageno es NULL. Si el valor devuelto esfalse, el valor demessagees NULL.
La regla para key se puede expresar de forma sucinta: key debe ser un tipo de referencia que no acepta valores NULL. El parámetro message es más complejo. Permite una variable que sea null como argumento, pero garantiza que, si se ejecuta correctamente, el argumento out no sea null. En estos escenarios, necesita un vocabulario más completo para describir las expectativas. El NotNullWhen atributo describe el estado null del argumento usado para el message parámetro .
Nota
Al agregar estos atributos, se proporciona más información al compilador sobre las reglas de la API. Al llamar al código se compila en un contexto habilitado que acepta valores NULL, el compilador advierte a los autores de llamadas cuando infringen esas reglas. Estos atributos no habilitan más comprobaciones en la implementación.
| Atributo | Category | Significado |
|---|---|---|
| AllowNull | Condición previa | Un parámetro, campo o propiedad que no acepta valores NULL puede ser NULL. |
| DisallowNull | Condición previa | Un parámetro, campo o propiedad que acepta valores NULL nunca debe ser NULL. |
| MaybeNull | Condición posterior | Un parámetro que no acepta valores NULL, campo, propiedad o valor devuelto puede ser NULL. |
| NotNull | Condición posterior | Un parámetro que acepta valores NULL, campo, propiedad o valor devuelto nunca es NULL. |
| MaybeNullWhen | Condición posterior condicional | Un argumento que no acepta valores NULL puede ser NULL cuando el método devuelve el valor especificado bool . |
| NotNullWhen | Condición posterior condicional | Un argumento que acepta valores NULL no es NULL cuando el método devuelve el valor especificado bool . |
| NotNullIfNotNull | Condición posterior condicional | Un valor devuelto, propiedad o argumento no es NULL si el argumento del parámetro especificado no es NULL. |
| MemberNotNull | Métodos del asistente para propiedades y métodos | El miembro enumerado no es NULL cuando el método devuelve. |
| MemberNotNullWhen | Métodos del asistente para propiedades y métodos | El miembro enumerado no es null cuando el método devuelve el valor especificado bool . |
| DoesNotReturn | Código inaccesible | Un método o propiedad nunca devuelve valores. Es decir, siempre inicia una excepción. |
| DoesNotReturnIf | Código inaccesible | Este método o propiedad nunca devuelve un valor si el parámetro bool asociado tiene el valor especificado. |
Las descripciones anteriores son una referencia rápida a lo que hace cada atributo. En las secciones siguientes se describe el comportamiento y el significado de estos atributos de forma más exhaustiva.
Condiciones previas: AllowNull y DisallowNull
Considere una propiedad de lectura y escritura que nunca devuelve null porque tiene un valor predeterminado razonable. Los autores de la llamada pasan null al descriptor de acceso set cuando lo establecen en el valor predeterminado. Por ejemplo, imagine un sistema de mensajería que solicita un nombre de pantalla en un salón de chat. Si no se proporciona ninguno, el sistema genera uno aleatorio:
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;
Al compilar el código anterior en un contexto en el que se desconocen los valores NULL, todo es correcto. Una vez que se habilitan los tipos de referencia que admiten un valor NULL, la propiedad ScreenName se convierte en una referencia que no acepta valores NULL. No es necesario que los autores de la llamada comprueben null en la propiedad devuelta. Pero ahora, al establecer la propiedad en null, se genera una advertencia. Para admitir este tipo de código, agregue el atributo System.Diagnostics.CodeAnalysis.AllowNullAttribute a la propiedad, como se muestra en el código siguiente:
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();
Es posible que tenga que agregar una using directiva para System.Diagnostics.CodeAnalysis que use este y otros atributos descritos en este artículo. El atributo se aplica a la propiedad, no al descriptor de acceso set. El atributo AllowNull especifica condiciones previas y solo se aplica a los argumentos. El descriptor de acceso get tiene un valor devuelto, pero no parámetros. Por tanto, el atributo AllowNull solo se aplica al descriptor de acceso set.
En el ejemplo anterior se muestra qué se debe buscar al agregar el atributo AllowNull en un argumento:
- El contrato general para esa variable es que no debe ser
null, por lo que quiere un tipo de referencia que no acepte valores NULL. - Existen escenarios para que un autor de llamada pase
nullcomo argumento, aunque no son el uso más común.
Con frecuencia, necesita este atributo para los argumentos , o in, outy ref . El atributo AllowNull es la mejor opción cuando una variable normalmente no es NULL, pero debe permitir null como condición previa.
Compare esto con los escenarios donde se usaDisallowNull: este atributo se usa para especificar que un argumento de un tipo de referencia que admite un valor NULL no deba ser null. Considere una propiedad donde null es el valor predeterminado, pero los clientes solo pueden establecerla en un valor que no sea NULL. Observe el código siguiente:
public string ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;
El código anterior es la mejor manera de expresar el diseño de que ReviewComment pueda ser null, pero no se puede establecer en null. Una vez que este código admita valores NULL, puede expresar este concepto con más claridad para los autores de la llamada mediante System.Diagnostics.CodeAnalysis.DisallowNullAttribute:
[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;
En un contexto que admite un valor NULL, el descriptor de acceso ReviewComment de get podría devolver el valor predeterminado de null. El compilador advierte que se debe comprobar antes del acceso. Además, advierte a los autores de la llamada que, aunque podría ser null, no deberían establecerlo de forma explícita en null. El atributo DisallowNull también especifica una condición previa, no afecta al descriptor de acceso get. Use el atributo DisallowNull cuando observe estas características:
- La variable podría ser
nullen escenarios principales, a menudo cuando se crea su primera instancia. - La variable no se debe establecer de forma explícita en
null.
Estas situaciones son comunes en el código en el que originalmente se desconocían los valores NULL. Es posible que las propiedades del objeto se establezcan en dos operaciones de inicialización distintas. Es posible que algunas propiedades se establezcan solo después de que se complete algún trabajo asincrónico.
Los AllowNull atributos y DisallowNull permiten especificar que las condiciones previas de las variables podrían no coincidir con las anotaciones que aceptan valores NULL en esas variables. Estas anotaciones proporcionan más detalles sobre las características de la API. Esta información adicional ayuda a los autores de la llamada a usar la API de manera correcta. Recuerde que debe especificar las condiciones previas mediante los atributos siguientes:
- AllowNull: un argumento que no acepta valores NULL puede ser NULL.
- DisallowNull: un argumento que admite un valor NULL nunca debe ser NULL.
Condiciones posteriores: MaybeNull y NotNull
Imagine que tiene un método con la siguiente firma:
public Customer FindCustomer(string lastName, string firstName)
Es probable que haya escrito un método similar a este para devolver null cuando no se encontró el nombre buscado.
null indica claramente que no se ha encontrado el registro. En este ejemplo, es probable que cambie el tipo de valor devuelto de Customer a Customer?. Al declarar el valor devuelto como un tipo de referencia que admite un valor NULL, se especifica claramente la intención de esta API:
public Customer? FindCustomer(string lastName, string firstName)
Por motivos descritos en Nulabilidad genérica , esa técnica podría no generar el análisis estático que coincida con la API. Es posible que tenga un método genérico que siga un patrón similar:
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
El método devuelve null cuando no se encuentra el elemento buscado. Puede aclarar que el método devuelve null cuando no se encuentra un elemento agregando la anotación MaybeNull a la devolución del método:
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
El código anterior informa a los autores de llamadas de que el valor devuelto puede ser realmente NULL. También informa al compilador de que el método puede devolver una null expresión aunque el tipo no acepta valores NULL. Si tiene un método genérico que devuelve una instancia de su parámetro de tipo, T, puede expresar que nunca devuelve null mediante el atributo NotNull.
También puede especificar que un valor devuelto o un argumento no sean NULL aunque el tipo sea un tipo de referencia que admite un valor NULL. El método siguiente es un método auxiliar que se produce si su primer argumento es null:
public static void ThrowWhenNull(object value, string valueExpression = "")
{
if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}
Puede llamar a esta rutina de esta forma:
public static void LogMessage(string? message)
{
ThrowWhenNull(message, $"{nameof(message)} must not be null");
Console.WriteLine(message.Length);
}
Después de habilitar los tipos de referencia NULL, querrá asegurarse de que el código anterior se compila sin advertencias. Cuando el método devuelve un valor, se garantiza que el parámetro value no es NULL. Pero es aceptable llamar a ThrowWhenNull con una referencia nula. Puede convertir a value en un tipo de referencia que acepte valores NULL y agregar la condición posterior NotNull a la declaración del parámetro:
public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
_ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
// other logic elided
En el código anterior se expresa con claridad el contrato existente: los autores de la llamada pueden pasar una variable con el valor null, pero se garantiza que el valor devuelto nunca será NULL.
Las condiciones posteriores condicionales se especifican mediante los atributos siguientes:
- MaybeNull: un valor devuelto que no acepta valores NULL puede ser NULL.
- NotNull: un valor devuelto que acepta valores NULL nunca es NULL.
Condiciones posteriores condicionales: NotNullWhen, MaybeNullWhen y NotNullIfNotNull
Es probable que esté familiarizado con el método string de String.IsNullOrEmpty(String). Este método devuelve true cuando el argumento es NULL o una cadena vacía. Es una forma de comprobación de valores NULL: No es necesario que los autores de la llamada comprueben los valores NULL del argumento si el método devuelve false. Para hacer que un método como este admita valores NULL, tendría que establecer el argumento en un tipo de referencia que admite un valor NULL y agregar el atributo NotNullWhen:
bool IsNullOrEmpty([NotNullWhen(false)] string? value)
Esto informa al compilador de que no es necesario comprobar los valores NULL en el código cuyo valor devuelto sea false. La adición del atributo informa al análisis estático del compilador que IsNullOrEmpty realiza la comprobación de valores NULL necesaria: cuando devuelve false, el argumento no es null.
string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.
El String.IsNullOrEmpty(String) método es como se muestra en el ejemplo anterior. Es posible que tenga métodos similares en el código base que comprueben el estado de los objetos para los valores NULL. El compilador no reconoce métodos de comprobación null personalizados y debe agregar las anotaciones usted mismo. Al agregar el atributo , el análisis estático del compilador sabe cuándo la variable probada está activada en null.
Otro uso de estos atributos es el patrón Try*. Las condiciones posteriores para los argumentos ref y out se comunican a través del valor devuelto. Considere este método mostrado anteriormente (en un contexto deshabilitado que admite un valor NULL):
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
El método anterior sigue una expresión típica de .NET: el valor devuelto indica si message se estableció en el valor buscado o, si no se encuentra ningún mensaje, en el valor predeterminado. Si el método devuelve true, el valor de message no es NULL; de lo contrario, el método establece message en NULL.
En un contexto habilitado que admite un valor NULL, puede comunicar esa expresión mediante el atributo NotNullWhen. Al anotar parámetros para los tipos de referencia que admiten un valor NULL, convierta message en string? y agregue un atributo:
bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message is not null;
}
En el ejemplo anterior, se sabe que el valor de message no es NULL cuando TryGetMessage devuelve true. Debe anotar de la misma forma los métodos similares en el código base: los argumentos pueden ser iguales a null, y se sabe que no son NULL cuando el método devuelve true.
También es posible que necesite un atributo final. En ocasiones, el estado NULL de un valor devuelto depende del estado NULL de uno o más argumentos. Estos métodos devuelven un valor distinto de NULL siempre que determinados argumentos no nullsean . Para anotar correctamente estos métodos, use el atributo NotNullIfNotNull. Observe el método siguiente:
string GetTopLevelDomainFromFullUrl(string url)
Si el argumento url no es NULL, el resultado no es null. Una vez habilitadas las referencias que aceptan valores NULL, debe agregar más anotaciones si la API puede aceptar un argumento NULL. Puede anotar el tipo de valor devuelto como se muestra en el código siguiente:
string? GetTopLevelDomainFromFullUrl(string? url)
Esto también funciona, pero a menudo fuerza a los autores de llamadas a implementar comprobaciones adicionales null . El contrato es que el valor devuelto solo será null cuando el argumento url sea null. Para expresar ese contrato, tendría que anotar este método como se muestra en el código siguiente:
[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)
En el ejemplo anterior se usa el operador nameof para el parámetro url. El valor devuelto y el argumento se han anotado con ?, lo que indica que cualquiera podría ser null. El atributo aclara aún más que el valor devuelto no es NULL cuando el url argumento no nulles .
Las condiciones posteriores condicionales se especifican mediante estos atributos:
-
MaybeNullWhen: un argumento que no acepta valores NULL puede ser NULL cuando el método devuelve el valor especificado
bool. -
NotNullWhen: un argumento que acepta valores NULL no es NULL cuando el método devuelve el valor especificado
bool. - NotNullIfNotNull: un valor devuelto no es NULL si el argumento del parámetro especificado no es NULL.
Métodos del asistente :MemberNotNull y MemberNotNullWhen
Estos atributos especifican la intención al refactorizar código común de constructores en métodos auxiliares. El compilador de C# analiza constructores e inicializadores de campo para asegurarse de que todos los campos de referencia que no aceptan valores NULL se inicializan antes de que cada constructor devuelva. Sin embargo, el compilador de C# no realiza un seguimiento de las asignaciones de campo a través de todos los métodos auxiliares. El compilador emite una advertencia CS8618 cuando los campos no se inicializan directamente en el constructor, sino en un método auxiliar. Agregue MemberNotNullAttribute a una declaración de método y especifique los campos que se inicializan en un valor distinto de NULL en el método. Por ejemplo, considere el siguiente ejemplo:
public class Container
{
private string _uniqueIdentifier; // must be initialized.
private string? _optionalMessage;
public Container()
{
Helper();
}
public Container(string message)
{
Helper();
_optionalMessage = message;
}
[MemberNotNull(nameof(_uniqueIdentifier))]
private void Helper()
{
_uniqueIdentifier = DateTime.Now.Ticks.ToString();
}
}
Puede especificar varios nombres de campo como argumentos para el constructor de atributo MemberNotNull.
MemberNotNullWhenAttribute tiene un argumento bool. Utiliza MemberNotNullWhen en situaciones en las que el método auxiliar devuelve bool, lo cual indica si el método auxiliar ha inicializado los campos.
Detención del análisis que admite un valor NULL cuando un método al que se le llama genera un valor
Algunos métodos, normalmente asistentes de excepciones u otros métodos de utilidad, siempre salen iniciando una excepción. O bien, un asistente produce una excepción basada en el valor de un argumento booleano.
En el primer caso, puede agregar el atributo DoesNotReturnAttribute a la declaración del método. El análisis del estado NULL del compilador no comprueba ningún código de un método que siga una llamada a un método anotado con DoesNotReturn. Considere este método:
[DoesNotReturn]
private void FailFast()
{
throw new InvalidOperationException();
}
public void SetState(object containedField)
{
if (containedField is null)
{
FailFast();
}
// containedField can't be null:
_field = containedField;
}
El compilador no emite ninguna advertencia después de la llamada a FailFast.
En el segundo caso, se agrega el atributo System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute a un parámetro booleano del método. Puede modificar el ejemplo anterior de esta manera:
private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
if (isNull)
{
throw new InvalidOperationException();
}
}
public void SetFieldState(object? containedField)
{
FailFastIf(containedField == null);
// No warning: containedField can't be null here:
_field = containedField;
}
Cuando el valor del argumento coincide con el valor del constructor DoesNotReturnIf, el compilador no realiza ningún análisis de estado NULL después de ese método.
Resumen
Agregar tipos de referencia que aceptan valores NULL proporciona un vocabulario inicial para describir las expectativas de las API para las variables que podrían ser null. Los atributos proporcionan un vocabulario más completo para describir el estado NULL de las variables como condiciones previas y posteriores. Estos atributos describen con más claridad las expectativas y proporcionan una mejor experiencia para los desarrolladores que usan las API.
A medida que actualice las bibliotecas para un contexto que admite un valor NULL, agregue estos atributos para guiar a los usuarios de las API al uso correcto. Estos atributos ayudan a describir de forma completa el estado NULL de los argumentos y los valores devueltos.
- AllowNull: un campo, parámetro o propiedad que no acepta valores NULL puede ser NULL.
- DisallowNull: un campo, parámetro o propiedad que admite un valor NULL nunca debe ser NULL.
- MaybeNull: un campo, parámetro, propiedad o valor devuelto que no acepta valores NULL podría ser NULL.
- NotNull: un campo, parámetro, propiedad o valor devuelto que acepta valores NULL nunca es NULL.
-
MaybeNullWhen: un argumento que no acepta valores NULL puede ser NULL cuando el método devuelve el valor especificado
bool. -
NotNullWhen: un argumento que acepta valores NULL no es NULL cuando el método devuelve el valor especificado
bool. - NotNullIfNotNull: un parámetro, propiedad o valor devuelto no es NULL si el argumento del parámetro especificado no es NULL.
- DoesNotReturn; un método o propiedad nunca devuelve valores. Es decir, siempre inicia una excepción.
-
DoesNotReturnIf: este método nunca devuelve un valor si el parámetro
boolasociado tiene el valor especificado.