El error mas recurrente en aplicaciones C# de cualquier tamano es la NullReferenceException. Durante muchos anos, el compilador no diferenciaba entre una variable que podia valer nulo y otra que nunca deberia serlo. Los nullable reference types cambian esa situacion y convierten la intencion del desarrollador en una restriccion verificable por el compilador.
Activacion y anotaciones basicas
La funcionalidad se activa en el archivo de proyecto mediante la propiedad Nullable con valor enable. Una vez habilitada, todas las referencias pasan a ser no anulables por defecto. Asignar null a una variable de tipo string genera un aviso.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
La sintaxis para declarar una referencia que si admite null es el signo de interrogacion despues del tipo. Sin el, la variable se considera garantizada.
string nombre = "Lucia";
string? apellido = null;
nombre = null;
apellido = "Perez";
La primera asignacion a null produce un aviso del compilador porque nombre se declaro como no anulable. La segunda es valida. Esta diferencia visual permite ver en cada declaracion si una variable puede o no ser nula sin abrir la documentacion.
Un aviso de nulabilidad no detiene la compilacion por defecto, pero si se trata como error con
<TreatWarningsAsErrors>o con reglas de analisis. En entornos profesionales es habitual subir el nivel.
Flujo de datos y analisis estatico
El compilador realiza un analisis de flujo para saber cuando una referencia anulable ha sido comprobada. Despues de un if que descarte null, la variable pasa a considerarse segura dentro del bloque.
public int LongitudDelNombre(string? texto)
{
if (texto is null)
{
return 0;
}
return texto.Length;
}
Dentro del return texto.Length, el compilador sabe que texto no es nulo y no emite aviso. El mismo analisis funciona con patrones mas avanzados como is not null, is { } o comparaciones con cadenas vacias.
public string Saludar(string? nombre)
{
if (string.IsNullOrEmpty(nombre))
{
return "Hola, desconocido.";
}
return $"Hola, {nombre.ToUpper()}.";
}
Las tablas de atributos como NotNullWhen, MaybeNullWhen o MemberNotNull amplian el analisis para metodos como TryParse. Estos atributos viven en System.Diagnostics.CodeAnalysis y son la razon por la que el compilador entiende, sin ayuda adicional, que tras un TryParse(x, out var y) devuelto a true, la variable y ya no es nula.
Operadores de nulabilidad
C# ofrece un conjunto de operadores especificos para trabajar con referencias anulables sin escribir ramas if redundantes. Cada uno cubre un caso tipico.
El operador de acceso condicional ?. detiene la cadena si el valor intermedio es nulo y devuelve null como resultado.
string? ciudad = persona?.Direccion?.Ciudad;
Si persona o persona.Direccion son nulos, la expresion completa vale null. No hay excepcion y el resultado queda tipado como string?.
El operador de fusion de nulos ?? aporta un valor por defecto cuando el lado izquierdo es nulo.
string ciudadMostrada = ciudad ?? "Sin asignar";
La variante ??= asigna el valor por defecto solo si la variable es nula, evitando asignaciones innecesarias cuando ya hay un valor presente.
cache ??= new Dictionary<string, int>();
cache["visitas"] = 1;
El operador de indice condicional ?[ ] se aplica a colecciones para acceder con seguridad a un elemento cuando la coleccion entera puede ser nula.
string? primero = usuarios?[0];
Combinar
?.con metodos permite llamar a un metodo solo cuando la referencia existe, descartando la llamada cuando es nula sin romper el flujo.
Operador de supresion y su uso responsable
El operador de supresion de nulabilidad, conocido como null-forgiving, se escribe con un signo de exclamacion despues de la expresion. Indica al compilador que el desarrollador garantiza un valor no nulo, aunque el analisis estatico no pueda probarlo.
public string LeerNombre(Dictionary<string, string?> datos)
{
return datos["nombre"]!.ToUpper();
}
El operador evita un aviso, pero no genera codigo. Si la promesa era falsa, se produce igualmente una NullReferenceException en tiempo de ejecucion. Por eso su uso debe ser puntual y documentado.
Un caso donde si aplica es en tests, cuando un helper garantiza que un valor esta presente antes de usarlo. Otro caso legitimo es la interoperabilidad con APIs antiguas que no estan anotadas para nulabilidad. En codigo nuevo es preferible reestructurar la logica antes que recurrir al !.
var resultado = servicio.TryBuscar(id, out var encontrado)
? encontrado!
: throw new InvalidOperationException($"Elemento {id} no hallado.");
Si un archivo esta lleno de
!conviene revisar el diseno. Probablemente haya un metodo que deberia lanzar antes de devolver o un modelo que deberia hacer obligatorio un campo.
Propiedades y parametros no anulables
Las propiedades de referencia declaradas sin interrogacion deben inicializarse antes de salir del constructor. De lo contrario, el compilador avisa de un posible estado invalido. Existen varias formas de cumplir la promesa.
La primera es asignar un valor inicial en la declaracion.
public class Usuario
{
public string Nombre { get; set; } = string.Empty;
public List<string> Roles { get; } = new();
}
La segunda es inicializar en el constructor.
public class Pedido
{
public string Referencia { get; }
public DateTime Fecha { get; }
public Pedido(string referencia, DateTime fecha)
{
Referencia = referencia;
Fecha = fecha;
}
}
En parametros de metodo, un tipo sin interrogacion significa que no se espera recibir nulo. Quien llame al metodo recibira un aviso si pretende pasar una referencia anulable.
public void Registrar(string correo)
{
Console.WriteLine(correo.ToLower());
}
string? entrada = ObtenerEntrada();
Registrar(entrada);
El ejemplo anterior produce un aviso porque entrada es string? y el parametro espera string. La solucion correcta es comprobar antes de llamar o declarar el parametro como string?.
Excepciones explicitas en los limites
En los bordes de la aplicacion, como APIs publicas o parseos de entrada, conviene validar los nulos con excepciones claras en lugar de confiar ciegamente en el analisis estatico. El tipo ArgumentNullException tiene un helper estatico muy conciso.
public class Repositorio
{
public void Guardar(Entidad entidad)
{
ArgumentNullException.ThrowIfNull(entidad);
BaseDeDatos.Insertar(entidad);
}
}
La llamada a ThrowIfNull lanza con un mensaje que incluye el nombre del parametro. Es una practica habitual en librerias expuestas a otros equipos, porque proporciona mensajes utiles incluso cuando el consumidor ignora los avisos de nulabilidad.
Para valores calculados que no deberian ser nulos, el tipo NullReferenceException no es la eleccion correcta. Es mejor lanzar InvalidOperationException con un mensaje que explique por que la situacion es inesperada.
public Producto BuscarOLanzar(int id)
{
return _repositorio.Buscar(id)
?? throw new InvalidOperationException($"Producto {id} no existe.");
}
El operador ?? combinado con throw sintetiza la comprobacion en una sola linea legible y preserva la garantia de no nulabilidad a partir de ese punto.
flowchart TD
A[Referencia entrante] --> B{Es string?}
B -->|si| C{Null?}
B -->|no| D[Uso directo seguro]
C -->|si| E[Retornar o lanzar]
C -->|no| F[Acceder propiedades y metodos]
Contexto de nulabilidad por archivo
A veces conviene habilitar o deshabilitar la nulabilidad para un archivo concreto. Las directivas #nullable enable y #nullable disable cambian el contexto para el resto del archivo y se pueden combinar con warnings o annotations para un control mas fino.
#nullable enable
namespace Facturacion.Integraciones;
public class ClienteLegacy
{
public string? Datos { get; set; }
}
En proyectos grandes, activar la nulabilidad de golpe genera cientos de avisos. La estrategia habitual es habilitarla globalmente en el .csproj y luego suprimir archivos concretos con #nullable disable hasta que se vayan migrando. El progreso se mide en numero de archivos sin supresiones.
Un archivo con
#nullable disablees un recordatorio visible de deuda tecnica. Tratarlos como tareas a resolver en sprints sucesivos lleva el proyecto a un estado completamente tipado.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, C# es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de C#
Explora más contenido relacionado con C# y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Dominar los nullable reference types de C# para distinguir referencias anulables y no anulables, aplicar operadores de nulabilidad y eliminar NullReferenceException en tiempo de compilacion.