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.
Tip
¿No está familiarizado con el desarrollo de software? Comience con los tutoriales de introducción .
¿Competente en otro idioma? Si ha trabajado con los tipos que aceptan valores NULL de Kotlin, las opciones opcionales de TypeScript strictNullCheckso Swift, el modelo es familiar. C# usa diagnósticos de advertencia y análisis estáticos en lugar de un tipo independiente. Eche un vistazo a Exprese la intención con anotaciones y a Análisis de estado nulo, y después vaya al Tutorial: Exprese la intención del diseño con tipos de referencia que aceptan valores NULL y que no los aceptan para aplicar esta característica.
Los tipos de referencia que aceptan valores NULL son un grupo de características que minimizan la posibilidad de que el código produzca System.NullReferenceException. Declara qué variables están pensadas para contener null y cuáles no, y el compilador advierte cuando esas declaraciones no coinciden con el modo en que el código los usa. El comportamiento en tiempo de ejecución del programa no cambia. Los tipos de referencia anulables son una característica exclusivamente de tiempo de compilación.
Tres componentes básicos trabajan juntos:
-
Anotaciones de variables (
stringfrente astring?) indican qué referencias están destinadas a permitirnull. - El análisis de estado NULL realiza un seguimiento de si el valor de una expresión no es null o tal vez-null en cada punto del código.
-
Los atributos de las API describen contratos más matizados, como "este argumento puede ser
null, pero el valor devuelto es NULL solo cuando el argumento es NULL".
El compilador combina estas señales para generar diagnósticos. Las advertencias en una variable que no acepta valores NULL significan que la variable puede recibir null. Las advertencias en una variable que acepta valores NULL significan que el código podría desreferenciarlo sin una comprobación nula.
La desreferencia significa usar el valor al que hace referencia la variable. Por ejemplo, para llamar a un método en él (variable.Method()), leer una propiedad (variable.Property) o indexar en él (variable[0]). Desreferenciar una variable con valor de null produce una excepción en tiempo de ejecución. Cualquier tipo de advertencia significa que el comportamiento del código no coincide con su diseño indicado.
Contexto que admite un valor NULL
Los proyectos creados a partir de plantillas de .NET recientes establecen <Nullable>enable</Nullable> en el archivo de proyecto, por lo que las instrucciones de este artículo se aplican como escritas. Si está trabajando en un proyecto anterior, abra .csproj y compruebe que <PropertyGroup> contiene la línea siguiente; agréguela si falta:
<Nullable>enable</Nullable>
Para obtener más información sobre la migración de una aplicación grande, consulte el artículo sobre estrategias de migración que aceptan valores NULL para obtener más configuraciones y directivas.
Expresa la intención con anotaciones
Cada variable de tipo de referencia no acepta valores NULL de forma predeterminada. Agregue ? para declarar un tipo de referencia anulable:
public static void Annotations()
{
string required = "always set"; // non-nullable: assigning null produces a warning
string? optional = null; // nullable: holding null is allowed
Console.WriteLine(required.Length);
if (optional is not null)
{
Console.WriteLine(optional.Length);
}
}
La anotación no cambia el tipo en tiempo de ejecución.
string y string? son ambos System.String.
? informa al compilador de la intención de diseño. Esa intención da forma a las advertencias que genera el compilador:
- Una variable que no acepta valores NULL tiene un estado null predeterminado de not-null. El compilador advierte si asigna un valor que podría ser
null. - Una variable que acepta valores NULL tiene un estado null predeterminado de maybe-null. El compilador advierte si desreferencia la variable sin comprobarla primero.
Use la anotación para hacer que los valores obligatorios y opcionales sean visibles en el sistema de tipos. El siguiente tipo Person declara FirstName y LastName como no anulables —toda persona debe tener ambos—, y MiddleName como anulable, porque no todo el mundo tiene uno:
public sealed class Person(string firstName, string lastName)
{
public string FirstName { get; } = firstName;
public string? MiddleName { get; init; }
public string LastName { get; } = lastName;
public override string ToString() => MiddleName is null
? $"{FirstName} {LastName}"
: $"{FirstName} {MiddleName} {LastName}";
}
public static void DesignIntent()
{
Person p1 = new("Ada", "Lovelace") { MiddleName = "King" };
Console.WriteLine(p1);
// Output: Ada King Lovelace
Person p2 = new("Grace", "Hopper");
Console.WriteLine(p2);
// Output: Grace Hopper
}
Las anotaciones impulsan la implementación de ToString. Como FirstName y LastName no aceptan valores nulos, la redefinición los usa directamente en una cadena interpolada (la sintaxis $"..." que inserta expresiones en marcadores de posición {}) sin ninguna comprobación de valores nulos.
MiddleName puede ser null, por lo que la sobrescritura lo comprueba primero con null y solo lo incluye si está presente. El compilador hace cumplir esta distinción: el código que pasa un valor que puede ser nulo donde se espera uno no anulable genera una advertencia, y un constructor que deja sin inicializar un miembro no anulable también genera una advertencia.
Análisis del estado nulo
El compilador realiza un seguimiento del estado NULL de cada expresión. El estado es uno de los dos valores:
-
not-null: se sabe que la expresión no es
null. -
maybe-null: la expresión podría ser
null.
El estado null de una variable local se actualiza a medida que el compilador analiza el código. Dos cosas lo cambian: asignaciones y comprobaciones nulas. Después de una asignación, el estado null de la variable coincide con la expresión en el lado derecho. Si la expresión es null o admite valores null, la variable pasa a ser posiblemente null. Si la expresión es un literal que no es NULL, la variable se convierte en not-null. Después de una comprobación nula, el estado null de la variable refleja la rama que se toma.
public static void NullStateTracking()
{
string? message = null;
// Warning: dereference of a possibly null reference.
Console.WriteLine(message.Length);
message = "Hello, World!";
// No warning: the compiler tracks that message is now not-null.
Console.WriteLine(message.Length);
}
En el ejemplo anterior, la primera desreferencia genera una advertencia porque messagetal vez es null. Después de la asignación a un literal que no sea NULL, el compilador sabe messageque no es null, por lo que la segunda desreferencia es segura.
El análisis de valores nulos funciona con las comprobaciones if, la comparación de patrones (expresiones como is null o is { } que comprueban la forma de un valor) y el flujo de control que realiza bucles o finaliza antes de tiempo:
public sealed class Node(string name)
{
public string Name { get; } = name;
public Node? Parent { get; init; }
}
public static void FlowAnalysis(Node start)
{
Node? current = start;
while (current is not null)
{
// Inside the loop, the compiler knows current is not-null.
Console.WriteLine(current.Name);
current = current.Parent;
}
}
El análisis no rastrea el interior de los métodos. Si necesita que un método comunique la nulabilidad a quienes lo llaman, use atributos de análisis de nulabilidad en su firma.
Invalidación de las advertencias con !
A veces sabe más que el compilador. El operador !null-forgiving declara que una expresión no es null, incluso cuando el análisis dice lo contrario:
public static void NullForgiving()
{
// "ada" matches a switch arm that returns a non-null string,
// but the return type is string? so the compiler treats the
// result as maybe-null.
string? maybeName = LookUpName("ada");
// The ! tells the compiler "trust me, this isn't null." We just
// passed "ada", which the switch maps to "Ada Lovelace".
int length = maybeName!.Length;
Console.WriteLine(length); // => 12
}
// Returns string? because the wildcard arm yields null.
private static string? LookUpName(string id) => id switch
{
"ada" => "Ada Lovelace",
_ => null,
};
Use ! con moderación. Cada caso es un punto en el que el compilador ya no puede protegerte. Prefiere agregar una comprobación nula, reestructurar el código o anotar la API pertinente para que el compilador llegue a la conclusión correcta por sí sola.
Atributos que describen contratos de API
Las anotaciones en un parámetro o tipo de valor devuelto no siempre son lo suficientemente expresivas. Un método puede aceptar un argumento posiblemente NULL, pero garantizar un resultado distinto de NULL. Un método de prueba puede devolverse true solo cuando su argumento no es NULL. Use los atributos de análisis que aceptan valores NULL para transmitir estos contratos:
public static bool IsPresent([NotNullWhen(true)] string? value) =>
!string.IsNullOrEmpty(value);
public static void NullAnalysisAttributes()
{
string? input = ReadInput();
if (IsPresent(input))
{
// No null-forgiving operator needed: the attribute tells the compiler
// input is not-null when IsPresent returns true.
Console.WriteLine(input.Length);
}
}
private static string? ReadInput() => "hello";
NotNullWhenAttribute indica al compilador que cuando IsPresent devuelve true, el argumento no es null. Dentro del if bloque, el compilador trata value como not-null sin que se requiera ningún operador que perdone null. A partir de .NET 5, todas las API del entorno de ejecución de .NET están anotadas, por lo que el análisis beneficia a cualquier código que las invoque.
Problemas conocidos
Hay dos patrones que pueden hacer que una referencia no anulable contenga null sin que se genere una advertencia. Ambos patrones son limitaciones del análisis estático, no errores en el código.
Estructuras predeterminadas
Puede crear una estructura con campos de referencia que no aceptan valores NULL mediante default o new(). Este enfoque deja sin inicializar los campos de la estructura:
public struct Student
{
public string FirstName;
public string? MiddleName;
public string LastName;
}
public static void DefaultStructPitfall()
{
Student s = default; // No warning, but FirstName and LastName are null.
Console.WriteLine(s.FirstName?.Length ?? -1);
}
Los campos se mantienen null en tiempo de ejecución, pero el compilador no advierte. Si debe usar una estructura, prefiera los miembros necesarios, que son miembros que el autor de la llamada debe inicializar a través de un inicializador de objeto o un constructor con parámetros que los autores de llamada deben invocar.
Matrices de referencias y estructuras
Una nueva matriz de un tipo de referencia no anulable contiene null en todos sus elementos hasta que se asigne cada uno de ellos:
public static void ArrayPitfall()
{
string[] values = new string[3]; // Elements are null at run time.
Console.WriteLine(values[0]?.Length ?? -1);
string[] initialized = ["a", "b", "c"]; // Collection expression initializes every slot.
Console.WriteLine(initialized[0].Length);
}
El mismo problema se aplica a las matrices de estructuras: cada elemento se inicia como el valor predeterminado del struct, por lo que los campos de referencia que no aceptan valores NULL de cada elemento se inician como null.
Inicialice los elementos de matriz como parte de la creación de la matriz.
Las expresiones de colección (la sintaxis literal [1, 2, 3]) y con tipo de destino new (escribir new() cuando el compilador puede deducir el tipo) permiten una inicialización completa concisa.