Entendendo Nullable Types no .NET
Os nullable types no .NET são o conjunto de recursos do C# que deixa explícito quais variáveis podem receber nulo e quais não podem, para que o compilador aponte riscos antes da execução. Afinal, lidar com possíveis valores nulos é sempre um desafio em qualquer linguagem de programação, e o .NET tem uma ótima forma de ajudar com isso.
Neste artigo, vamos entender de onde vêm os avisos de referência nula, o que é o null de verdade, por que tipos de valor e tipos de referência se comportam de forma diferente e como resolver cada situação no código.
O problema
Se você atualizou seu projeto para o .NET 6 ou superior, ou se começou um projeto recentemente, é bem possível que tenha visto diversos warnings (avisos) sobre possíveis referências nulas no seu código.
warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor.
Embora esse aviso não impeça a compilação da aplicação, ele indica que existe um possível valor nulo no código. Esse valor nulo, por sua vez, é a causa do famoso NullReferenceException.
O que mudou no .NET 6 com os nullable types
Os nullable reference types chegaram ao C# 8, mas ficavam desligados por padrão. A partir do .NET 6, no entanto, todos os modelos de projeto passaram a incluir no .csproj uma configuração chamada <Nullable>, conforme o exemplo abaixo.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <!-- 👈 -->
</PropertyGroup>
</Project>
Por isso, os avisos aparecem com tanta frequência em projetos novos. Segundo a referência de tipos de referência anuláveis do C#, antes do .NET 6 os projetos usavam disable como padrão.
A propriedade aceita os valores enable, disable, warnings e annotations. Também é possível ativar o recurso em um único arquivo com a diretiva #nullable enable.
Embora seja plenamente possível remover essa configuração ou atribuir o valor disable, a recomendação é mantê-la ligada. A própria Microsoft afirma que não recomenda desativar a verificação de nulos em projetos de produção.
O que é NULL
Antes de falar sobre como resolver o problema no código, precisamos entender o que é o null e quais problemas reais ele causa.
O .NET possui duas categorias de tipos: os tipos de referência (reference types) e os tipos de valor (value types). É importante frisar que todo tipo de valor recebe um valor padrão diferente de null.
Na verdade, qualquer tipo pode se tornar nulo, como veremos mais adiante com os nullable types. Porém, por padrão, todo tipo de valor no .NET começa com o que chamamos de valor padrão.
No caso dos números (int, double, float, decimal), esse valor é zero. Já um bool começa como false, um char começa como '\0' e assim por diante.
Mesmo tipos complexos, como estruturas (struct), que também são tipos de valor, recebem um valor padrão. Um bom exemplo é o DateTime, que, sem nenhum valor atribuído, assume 1/1/0001 12:00:00 AM.
int quantity = default; // 0
bool isActive = default; // false
DateTime createdAt = default; // 1/1/0001 12:00:00 AM
string? name = default; // null
Por que um tipo de valor não é nulo
Se você já fez nosso curso gratuito de C#, aprendeu que os tipos no .NET têm um tamanho específico. Por exemplo, um int sempre ocupa 32 bits, enquanto um short sempre ocupa 16 bits.
Essa tipagem ajuda muito na alocação de memória. Afinal, todo programa vai para a memória antes de ser executado, e saber que uma estrutura precisa de X bytes facilita a organização dos dados.
Dessa forma, não faz sentido inicializar um tipo de valor com nulo. Qual seria a vantagem de pedir 4 bytes ao computador e não colocar nenhum valor lá?
Tipos de referência são os vilões
Sabemos, então, que os tipos de valor armazenam a própria informação. Mas e os tipos de referência, o que são eles?
Diferente dos tipos de valor, os tipos de referência armazenam apenas um endereço, ou seja, uma referência para o lugar da memória onde os dados estão. Esse comportamento traz mais dinamismo, porque permite trabalhar com estruturas de tamanho variável.
Um ótimo exemplo de tipo de referência é a string, que representa uma sequência de caracteres (char). Como podemos ter textos de qualquer tamanho, faz todo sentido tratá-los como referência.
Então, quando temos uma string, temos na verdade uma referência para o conjunto de caracteres que está em algum lugar da memória. Claro que existe muito mais por baixo dessa explicação, como literais internadas e imutabilidade, mas você pegou a ideia.
Resumindo, ao declarar uma string, dizemos algo como "olha, as informações desta string estão naquele endereço". Se esse endereço não aponta para lugar nenhum, a referência é nula.
Para entender como o runtime gerencia essas áreas da memória, vale ler também o artigo sobre o Garbage Collector no C#.
E qual o problema de ter uma referência nula
O maior problema de uma referência nula aparece quando tentamos acessar o seu valor. Sempre que fazemos isso no .NET, recebemos uma exceção do tipo NullReferenceException.
Um caso clássico acontece quando trabalhamos com listas e esquecemos de inicializá-las:
public class Order
{
public List<Product> Products { get; set; }
// ⚠️ Lista não inicializada, vai dar erro
}
var order = new Order();
order.Products.Add(product);
// ❌ NullReferenceException: Products é null
Isso vale para todo tipo de referência. Como consequência, sem ajuda do compilador, precisaríamos verificar se o objeto aponta para nulo antes de qualquer acesso.
💡 APRENDA C# DE GRAÇA com o curso Fundamentos do C#. São 144 aulas divididas em 14 módulos com mais de 12 horas de duração, totalmente gratuito e com certificado de conclusão.
Como os nullable types resolvem o problema
O .NET incluiu a configuração mencionada no começo do artigo para nos avisar de possíveis referências nulas. Assim, podemos tratá-las ainda em tempo de compilação, antes que virem erro em produção.
Com os nullable types ativados, toda variável de tipo de referência passa a ser não anulável por padrão. O compilador, então, faz uma análise de estado nulo e marca cada expressão como "não nula" ou "talvez nula".
Vale reforçar que o recurso existe apenas em tempo de compilação. Como explica a documentação de tipos de referência anuláveis, o comportamento do programa em execução não muda: string e string? continuam sendo o mesmo System.String.
Para seguir adiante, vamos usar a classe abaixo como exemplo:
public class Student
{
public string Name { get; set; }
// ⚠️ Non-nullable property 'Name' is uninitialized
}
Inicializando as propriedades
Como Name é do tipo string, precisamos inicializá-lo, e para tipos simples isso é relativamente fácil.
public class Student
{
public string Name { get; set; } = string.Empty;
// ✅ OK
}
public class StudentWithDefaultName
{
public string Name { get; set; } = "André";
// ✅ OK
}
Também podemos inicializar a propriedade pelo construtor. Nesse caso, o compilador entende que a propriedade não recebe valor de imediato, mas recebe assim que o objeto é construído.
public class Student
{
public Student()
{
Name = string.Empty;
// ✅ OK
}
public string Name { get; set; } // ✅ OK
}
É importante salientar que, se houver vários construtores, você precisa inicializar a propriedade em todos eles. Por isso, talvez seja mais simples usar o primeiro exemplo desta seção.
public class Student
{
protected Student()
{
// ⚠️ Continuará com o warning, pois Name não foi inicializado aqui
}
public Student(string name)
{
Name = name;
// ✅ OK
}
public string Name { get; set; } // ⚠️
}
Membros required
A partir do C# 11, disponível no .NET 7 e no .NET 8, existe uma alternativa elegante: o modificador required. Com ele, quem cria o objeto fica obrigado a informar o valor da propriedade.
public class Customer
{
public required string Name { get; set; }
}
var customer = new Customer { Name = "André" }; // ✅ OK
// var invalid = new Customer(); // ❌ erro de compilação
Dessa forma, o aviso CS8618 desaparece e o compilador garante que Name sempre terá um valor. A documentação do EF Core sobre nullable reference types chega a apontar os membros required como solução ideal para entidades.
Nullable types para tipos de valor e de referência
E se, por acaso, eu quiser que uma propriedade seja nula? Isso é possível e bastante comum, principalmente quando trabalhamos com Entity Framework, em que uma coluna do banco pode não ter valor.
Nesse caso, o .NET oferece uma opção simples e elegante: adicionar ? ao tipo. Porém, os nullable types funcionam de forma diferente para tipos de valor e para tipos de referência.
Nullable value types e a estrutura Nullable
Para tipos de valor, o ? é um syntax sugar (um apelido) para a estrutura Nullable<T>. Assim, tanto faz escrever Nullable<DateTime> ou DateTime?.
public Nullable<DateTime> CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
Um bool?, por exemplo, aceita true, false ou null. A estrutura oferece as propriedades HasValue e Value, além do método GetValueOrDefault().
int? score = null;
Console.WriteLine(score.HasValue); // False
int finalScore = score ?? -1; // -1
int safeScore = score.GetValueOrDefault(); // 0
Cuidado ao ler Value sem verificar antes. Quando HasValue é false, essa propriedade lança um InvalidOperationException, como mostra a referência de nullable value types.
Nullable reference types com o operador ?
Para tipos de referência, o ? não cria um Nullable<T>. Na verdade, ele é apenas uma anotação que informa ao compilador a sua intenção, tanto que Nullable<string> nem compila.
Em resumo, sempre que você quiser deixar explícito que um tipo pode receber nulo, use o ? depois do tipo.
public class Student
{
public Name? Name { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class Name
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
Dessa forma, o compilador não exibe mais o aviso de inicialização, já que você mesmo declarou que aceita nulos nessas propriedades. Em troca, ele passa a avisar sempre que você tentar acessar Name.FirstName sem antes verificar se Name é nulo.
if (student.Name is not null)
{
Console.WriteLine(student.Name.FirstName);
}
// ou, de forma mais curta
Console.WriteLine(student.Name?.FirstName ?? "No name");
No EF Core, essa anotação também tem efeito no banco. Com os nullable types ativados, uma propriedade string? vira coluna opcional e uma string vira coluna obrigatória.
Null NOT, o operador null!
Como nada na vida é simples, vamos supor a seguinte situação:
public class Student
{
public Name Name { get; set; }
// ⚠️ Non-nullable property 'Name' is uninitialized
public DateTime? UpdatedAt { get; set; }
}
public class Name
{
public Name(string firstName, string lastName)
{
if (string.IsNullOrEmpty(firstName) ||
string.IsNullOrEmpty(lastName))
throw new ArgumentNullException("First name and last name are required");
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; set; }
public string LastName { get; set; }
}
Neste cenário, temos uma classe que define o nome e a usamos na classe Student, cujo nome não pode ser nulo. Para piorar, não conseguimos instanciar um Name sem informar o primeiro e o último nome, já que a classe não tem um construtor sem parâmetros (parameterless).
public Name Name { get; set; } = new();
// ❌ Não funciona, pois Name exige primeiro e último nome
public Name Name { get; set; } = new("", "");
// ❌ Não podemos inicializar com strings vazias
public Name Name { get; set; } = new("Bruce", "Wayne");
// ❌ Não queremos ter um valor padrão
Esse cenário é a definição perfeita para o uso do que costumo chamar de null NOT, ou null! (null seguido de uma exclamação). Oficialmente, o ! se chama operador de tolerância a nulo (null-forgiving).
O null NOT permite dizer o seguinte ao compilador:
Esta propriedade não pode ser nula, mas eu não tenho um valor para ela neste momento. No entanto, eu me comprometo a inicializá-la antes de utilizá-la.
Basicamente, puxamos a responsabilidade para nós e garantimos que esse valor, embora nulo agora, receberá um valor no futuro.
public Name Name { get; set; } = null!;
// ✅ OK
Segundo a referência do operador null-forgiving, ele não tem nenhum efeito em tempo de execução. Ou seja, se você esquecer de inicializar a propriedade, o NullReferenceException continua possível.
Por isso, use o null! com moderação. Um uso clássico e recomendado pela Microsoft aparece no EF Core, em que o DbContext inicializa as propriedades DbSet por reflexão.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Student> Students { get; set; } = null!;
}
Null NOT não é NOT NULL
É importante frisar que o null NOT não tem relação com o NOT NULL. O NOT NULL é uma comparação, com implementação e finalidade totalmente diferentes.
if (name is not null)
{
// name tem valor aqui
}
if (name != null)
{
// name tem valor aqui
}
Enquanto o null! só silencia o compilador, essas comparações verificam o valor em tempo de execução. Além disso, depois de um is not null, a análise de estado nulo já entende que a variável é segura dentro do bloco.
Boas práticas com nullable types no .NET
Primeiro, mantenha o <Nullable>enable</Nullable> em todos os projetos novos. Em projetos antigos, ative o recurso aos poucos, arquivo por arquivo, com a diretiva #nullable enable.
Em seguida, prefira inicializar coleções na própria declaração, como public List<Product> Products { get; set; } = [];. Segundo a documentação do EF Core, navegações de coleção devem ser sempre não anuláveis, porque uma lista vazia já representa a ausência de itens.
Além disso, reserve o ? para dados que realmente podem faltar, como um nome do meio ou uma data de atualização. Assim, os nullable types documentam o seu modelo de domínio de forma clara.
Também trate os avisos como erros quando o time estiver maduro no uso do recurso. Para isso, você pode usar a propriedade <WarningsAsErrors>nullable</WarningsAsErrors> no .csproj.
Por fim, use o null! apenas quando outro mecanismo garantir a inicialização, como o EF Core ou um framework de injeção. Para acompanhar as novidades mais recentes sobre nulos na linguagem, confira também as mudanças no null do .NET 10 e C# 14.
Conclusão
Entender como os valores nulos surgem e onde são usados é fundamental, assim como manter as configurações que ajudam o compilador a identificar possíveis referências nulas.
O .NET possui recursos incríveis, tanto para declarar nullable types quanto para tratar referências nulas. Com ?, Nullable<T>, required e null!, você tem uma ferramenta certa para cada situação.
Use o compilador e as configurações a seu favor, otimize seu código, evite o excesso de condicionais e divirta-se com o conteúdo que aprendeu aqui 🚀
FAQ
O que é o aviso CS8618?
É o aviso de que uma propriedade não anulável terminou o construtor sem valor. Você resolve inicializando a propriedade, usando required ou declarando o tipo como anulável.
Qual a diferença entre int? e string??
O int? é um Nullable<int>, uma estrutura real em tempo de execução. Já o string? é só uma anotação para o compilador e continua sendo System.String.
O null! evita o NullReferenceException?
Não, ele apenas silencia o aviso do compilador. Se a propriedade não receber valor, a exceção continua acontecendo.
Devo desativar o Nullable no projeto?
Não é recomendado em código de produção. O ideal é manter a configuração ativa e corrigir os avisos.
Desde quando o Nullable vem ativado por padrão?
Desde o .NET 6, os modelos de projeto incluem <Nullable>enable</Nullable>. O recurso em si chegou no C# 8.