null significa "ningún valor". Los tipos de referencia (clases, strings, arrays) siempre han podido contenerlo. Los tipos de valor (int, decimal, bool, DateTime, structs) no pueden, salvo que los hagas nullable con ?. C# te da entonces un pequeño conjunto de operadores para trabajar con valores que pueden faltar: ??, ??= y ?..
Tipos de valor nullable: int?
Añade ? a un tipo de valor para permitir null. int? es la forma abreviada del struct Nullable<int>, que contiene un valor más un indicador que dice si el valor está presente:
Salida:
False
True
True
5.50
0
3
Rated 4 stars
4
Las fuentes típicas de tipos de valor nullable son los campos opcionales de un formulario, los parámetros de consulta y las columnas NULL de una base de datos: un Order con un DateTime? ShippedAt no se ha enviado mientras el valor sea null. La alternativa, un valor mágico como -1 o DateTime.MinValue, parece un dato real y tarde o temprano se usa como tal.
Un int? solo se convierte a int de forma explícita, porque la conversión puede fallar. Eso nos lleva a la única excepción que lanzan los tipos de valor nullable.
"Nullable object must have a value"
Leer .Value cuando no hay valor, o convertir con un cast un int? null a int, lanza InvalidOperationException:
Salida:
InvalidOperationException: Nullable object must have a value.
0
0
not stocked
stock is int count es un patrón: comprueba si hay valor y lo desenvuelve en count en un solo paso, lo que se lee mejor que HasValue más Value. La página de pattern matching tiene más formas.
Aritmética y comparaciones con null
Los operadores sobre tipos de valor nullable están elevados: si cualquiera de los operandos es null, el resultado es null. Las comparaciones de orden (<, >, <=, >=) son la excepción: devuelven false en lugar de null. (La otra excepción es bool? con & y |, donde null & false es false.) Por ejemplo:
Salida:
True
[]
False
False
True
True
Tanto a > 5 como a <= 5 son falsos, así que !(a > 5) no es lo mismo que a <= 5 cuando a es null. Un código como if (!(age >= 18)) Deny(); trata una edad que falta como si fuera de un menor, lo que puede ser o no lo que querías. Decide primero qué debe significar null, normalmente con ??, y después compara.
El operador de fusión de null ??
a ?? b se evalúa a a cuando a no es null, y a b en caso contrario. Funciona con los tipos de valor nullable y con los tipos de referencia, y se encadena de izquierda a derecha:
Salida:
dark
16px
20
ANONYMOUS
El lado derecho solo se evalúa cuando hace falta, así que cache ?? LoadFromDatabase() no toca la base de datos cuando la caché tiene un valor. ?? también acepta una expresión throw, lo que da una guarda compacta (consulta throw):
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
??= (C# 8)
x ??= y asigna y a x solo si x es null. Es la forma idiomática de la inicialización perezosa:
private List<string> tags;
public void AddTag(string tag)
{
tags ??= new List<string>(); // C# 8 and later
tags.Add(tag);
}
Antes de C# 8, escribe tags = tags ?? new List<string>(); o if (tags == null) tags = new List<string>();.
El operador condicional null ?.
a?.Member evalúa Member solo si a no es null; si lo es, toda la expresión es null. Convierte una cadena de comprobaciones de null en una sola expresión:
Salida:
Braga
(no city)
(no customer)
0
True
handler: saved
Detalles que conviene conocer:
- El operador corta el resto de la cadena: en
nobody?.Address?.City, en cuantonobodyes null no se ejecuta nada de lo que hay a la derecha. - Cuando el miembro devuelve un tipo de valor, el resultado pasa a ser nullable:
Orders?.Countesint?, y por eso necesita?? 0para volver a ser unint. ?.no puede ser el destino de una asignación antes de C# 14:customer?.Name = "x"no compila en las versiones anteriores.handler?.Invoke(...)es la forma habitual de lanzar un evento o llamar a un callback opcional.
Usa ?. donde null sea un estado legítimo. Repartirlo por todas partes para evitar excepciones oculta el bug que produjo el null inesperado, y el programa sigue adelante con datos que faltan en lugar de fallar donde está el problema.
Comprobar si algo es null
if (customer == null) { ... } // classic
if (customer is null) { ... } // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... } // C# 9
if (customer is { } c) { ... } // C# 8 property pattern: not null, and named
is null siempre hace una comprobación de null real, mientras que == llama a la sobrecarga del operador del tipo si la tiene, así que una sobrecarga mal escrita puede hacer que x == null mienta. Para los argumentos que no deben ser null, falla pronto:
public void Ship(Order order)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6+
// ...
}
Tipos de referencia nullable (C# 8)
Añadir ? a un tipo de valor cambia el tipo. Añadir ? a un tipo de referencia, desde C# 8, solo cambia lo que comprueba el compilador. Con la característica activada (<Nullable>enable</Nullable> en el archivo de proyecto, el valor por defecto en los proyectos nuevos desde .NET 6), string significa "nunca debería ser null" y string? significa "puede ser null", y el compilador avisa cuando el código no coincide:
#nullable enable
public class User
{
public string Email { get; set; } // warning CS8618: Non-nullable property 'Email'
// must contain a non-null value when exiting constructor.
public string? Nickname { get; set; } // allowed to be null
public int NicknameLength() => Nickname.Length;
// warning CS8602: Dereference of a possibly null reference.
public int SafeLength() => Nickname?.Length ?? 0; // no warning
}
string? input = Console.ReadLine();
string name = input; // warning CS8600: Converting null literal or possible
// null value to non-nullable type.
string checkedName = input ?? "guest"; // no warning
int len = input!.Length; // ! tells the compiler "trust me": no warning
El compilador sigue tus comprobaciones a lo largo del método: después de if (input != null), input cuenta como no null. El operador ! (que perdona el null) silencia un aviso cuando sabes más que el análisis; cada uno es una afirmación que el compilador no puede verificar, así que úsalos poco.
Dos hechos aclaran casi toda la confusión. Primero, solo son avisos: el programa se ejecuta igual, y un string todavía puede contener null en tiempo de ejecución si vino de código sin anotaciones. Segundo, string? no es Nullable<string>; no tiene HasValue.
Errores comunes
.Valuesin comprobar. LanzaInvalidOperationExceptioncuando es null. Usa??,GetValueOrDefault()ois int n.- Suponer que
!(x > 5)significax <= 5. No cuandoxes null: toda comparación de orden con null es falsa. - Tapar cada null con
?.. Silencia el síntoma y aleja el fallo de su causa. - Tratar los avisos de referencias nullable como ruido. Cada uno marca una línea que puede lanzar
NullReferenceException. Arregla el flujo o expresa la intención con?. - Valores mágicos en lugar de null.
-1,0yDateTime.MinValuepara "desconocido" parecen datos reales. Usaint?yDateTime?.
Preguntas frecuentes
¿Qué significa int? en C#?
int? es la forma abreviada de Nullable<int>: un int que también puede ser null. Los tipos de valor como int, decimal, bool y DateTime normalmente siempre contienen un valor; añadir ? les da un estado de "ningún valor", que es lo que necesitan un campo de formulario vacío o una columna NULL de una base de datos. Compruébalo con HasValue o != null y léelo con Value o GetValueOrDefault().
¿Qué hace ?? en C#?
a ?? b es el operador de fusión de null: se evalúa a a si a no es null, y a b en caso contrario. string name = input ?? "guest"; aporta un valor alternativo en una sola expresión. Se puede encadenar (a ?? b ?? c toma el primer valor que no sea null) y b solo se evalúa cuando hace falta. ??= (C# 8) asigna el lado derecho solo cuando la variable es null.
¿Qué hace ?. en C#?
a?.B es el operador condicional null: si a es null, toda la expresión es null y B nunca se evalúa, así que no hay NullReferenceException. Se puede encadenar (order?.Customer?.Address?.City) y funciona con llamadas a métodos (logger?.Log(...)), eventos (Changed?.Invoke(...)) e indexadores (items?[0]). Combínalo con ?? para un valor por defecto: order?.Customer?.Name ?? "unknown".
¿Qué provoca "Nullable object must have a value"?
Leer .Value de un tipo de valor nullable que es null, o convertirlo con un cast al tipo no nullable ((int)maybe), lanza InvalidOperationException con ese mensaje. Comprueba antes HasValue, o usa GetValueOrDefault(), ?? fallback o un patrón: if (maybe is int n) { ... }.
¿Cómo compruebo si algo es null en C#?
Funcionan tanto x == null como x is null; a is null (C# 7) no le afecta un tipo que sobrecargue ==, así que muchas bases de código lo prefieren. La negación es x != null, o x is not null en C# 9. Para rechazar argumentos null, ArgumentNullException.ThrowIfNull(x) (.NET 6) o _ = x ?? throw new ArgumentNullException(nameof(x)); en código más antiguo.
¿Qué son los tipos de referencia nullable en C#?
Una característica del compilador de C# 8, que se activa con <Nullable>enable</Nullable> (el valor por defecto en los proyectos nuevos de .NET 6+). Con ella activada, string significa "nunca null" y string? significa "puede ser null", y el compilador avisa cuando podrías desreferenciar null (CS8602) o asignar null a una variable no nullable (CS8600). Solo cambia los avisos, no el comportamiento en tiempo de ejecución.