Nullable reference types

Intermedio
C#
C#
Actualizado: 21/04/2026

El error más recurrente en aplicaciones C# de cualquier tamaño es la NullReferenceException. Durante muchos años, el compilador no diferenciaba entre una variable que podía valer nulo y otra que nunca debería serlo. Los nullable reference types cambian esa situación y convierten la intención del desarrollador en una restricción verificable por el compilador.

Activación y anotaciones básicas

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 sí admite null es el signo de interrogación después del tipo. Sin el, la variable se considera garantizada.

string nombre = "Lucia";
string? apellido = null;

nombre = null;
apellido = "Perez";

La primera asignación a null produce un aviso del compilador porque nombre se declaró como no anulable. La segunda es válida. Esta diferencia visual permite ver en cada declaración si una variable puede o no ser nula sin abrir la documentación.

Un aviso de nulabilidad no detiene la compilación por defecto, pero si se trata como error con <TreatWarningsAsErrors> o con reglas de análisis. En entornos profesionales es habitual subir el nivel.

Flujo de datos y análisis estático

El compilador realiza un análisis de flujo para saber cuándo una referencia anulable ha sido comprobada. Después 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 análisis funciona con patrones más avanzados como is not null, is { } o comparaciones con cadenas vacías.

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 amplían el análisis para métodos como TryParse. Estos atributos viven en System.Diagnostics.CodeAnalysis y son la razón 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 específicos para trabajar con referencias anulables sin escribir ramas if redundantes. Cada uno cubre un caso típico.

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 expresión completa vale null. No hay excepción y el resultado queda tipado como string?.

El operador de fusión 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 índice condicional ?[ ] se aplica a colecciones para acceder con seguridad a un elemento cuando la colección entera puede ser nula.

string? primero = usuarios?[0];

Combinar ?. con métodos permite llamar a un método solo cuando la referencia existe, descartando la llamada cuando es nula sin romper el flujo.

Operador de supresión y su uso responsable

El operador de supresión de nulabilidad, conocido como null-forgiving, se escribe con un signo de exclamación después de la expresión. Indica al compilador que el desarrollador garantiza un valor no nulo, aunque el análisis estático no pueda probarlo.

public string LeerNombre(Dictionary<string, string?> datos)
{
    return datos["nombre"]!.ToUpper();
}

El operador evita un aviso, pero no genera código. Si la promesa era falsa, se produce igualmente una NullReferenceException en tiempo de ejecución. Por eso su uso debe ser puntual y documentado.

Un caso donde sí aplica es en tests, cuando un helper garantiza que un valor está presente antes de usarlo. Otro caso legítimo es la interoperabilidad con APIs antiguas que no están anotadas para nulabilidad. En código nuevo es preferible reestructurar la lógica antes que recurrir al !.

var resultado = servicio.TryBuscar(id, out var encontrado)
    ? encontrado!
    : throw new InvalidOperationException($"Elemento {id} no hallado.");

Si un archivo está lleno de ! conviene revisar el diseño. Probablemente haya un método que debería lanzar antes de devolver o un modelo que debería hacer obligatorio un campo.

Propiedades y parámetros no anulables

Las propiedades de referencia declaradas sin interrogación deben inicializarse antes de salir del constructor. De lo contrario, el compilador avisa de un posible estado inválido. Existen varias formas de cumplir la promesa.

La primera es asignar un valor inicial en la declaración.

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 parámetros de método, un tipo sin interrogación significa que no se espera recibir nulo. Quien llame al método recibirá 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 parámetro espera string. La solución correcta es comprobar antes de llamar o declarar el parámetro como string?.

Excepciones explícitas en los límites

En los bordes de la aplicación, como APIs públicas o parseos de entrada, conviene validar los nulos con excepciones claras en lugar de confiar ciegamente en el análisis estático. El tipo ArgumentNullException tiene un helper estático 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 parámetro. Es una práctica habitual en librerías expuestas a otros equipos, porque proporciona mensajes útiles incluso cuando el consumidor ignora los avisos de nulabilidad.

Para valores calculados que no deberían ser nulos, el tipo NullReferenceException no es la elección correcta. Es mejor lanzar InvalidOperationException con un mensaje que explique por qué la situación 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 comprobación en una sola línea legible y preserva la garantía 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 métodos]

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 más 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 número de archivos sin supresiones.

Un archivo con #nullable disable es un recordatorio visible de deuda técnica. Tratarlos como tareas a resolver en sprints sucesivos lleva el proyecto a un estado completamente tipado.

Alan Sastre - Autor del tutorial

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 compilación.