C# Referências Anuláveis

Nas duas últimas aulas apareceram avisos como CS8600 e CS8602, e um ? estranho depois de string. Eles fazem parte de um sistema do C# chamado referências anuláveis (nullable reference types). A ideia é simples: você diz ao compilador quais textos (e, mais tarde, quais objetos) podem ficar em branco, e ele confere o programa inteiro, antes de rodar, procurando lugares onde um null pode causar uma NullReferenceException.

Você vai aprender
  • A diferença entre string ("sempre tem texto") e string? ("pode estar em branco").
  • Onde o sistema é ligado: o <Nullable>enable</Nullable> do projeto e o dotnet run arquivo.cs.
  • Como o compilador "acompanha" o código: depois de um if (nome is null) return;, ele sabe que nome tem texto.
  • Cada aviso real: CS8600, CS8602, CS8603, CS8604, CS8625 e CS8632.
  • O operador ! ("confie em mim") e por que ele é perigoso.
  • Como desligar (#nullable disable) e como deixar o sistema mais rígido (WarningsAsErrors).

Todos os programas desta aula rodaram com dotnet run arquivo.cs na máquina fictícia da Ana (usuária ana, computador notebook), com o .NET 10 SDK (C# 14), na pasta ~/cs.


A ideia antes do código: a etiqueta "pode estar vazio"

Na mudança de casa da Ana, algumas caixas estão sempre cheias ("LOUÇA") e outras podem chegar vazias ("ACHADOS E PERDIDOS"). Se a etiqueta avisa "pode estar vazia", quem abre a caixa olha dentro antes de enfiar a mão. Se a etiqueta não avisa, todo mundo confia que tem algo lá.

No C#, string é a caixa que promete ter texto, e string? é a caixa com a etiqueta "pode estar vazia". O compilador é o fiscal da mudança: ele reclama quando alguém põe "nada" numa caixa que prometia estar cheia, ou quando alguém enfia a mão numa caixa "pode estar vazia" sem olhar antes.


Um pouco de história: por que isso existe

Na aula de null você viu que int não aceita null, mas string aceita. Isso vale para todos os tipos de referência (textos, arrays e os objetos que você vai criar a partir de Classes e Objetos): a variável guarda um "endereço", e um endereço pode estar em branco.

Até o C# 7, qualquer string podia ser null a qualquer momento, e o compilador não dizia nada. O resultado eram programas que compilavam bem e quebravam com NullReferenceException na mão do usuário. O C# 8 (2019) trouxe as referências anuláveis: o ?, que já existia em int?, passou a valer também para tipos de referência.

São só avisos: o programa continua igual

Para um int, o ? cria um tipo novo (int? é Nullable<int>). Para uma string, não: string e string? são o mesmo tipo quando o programa roda. O ? é uma anotação para o compilador, que só gera avisos. Um programa cheio de avisos de nulidade compila e roda, e pode quebrar. Por isso, leve esses avisos a sério.


Onde o sistema está ligado

Num projeto criado com dotnet new console, ele já vem ligado. Olhe o arquivo do projeto:

$ dotnet new console -o Cadastro
O modelo "Aplicativo do Console" foi criado com êxito.
…
$ cat Cadastro/Cadastro.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>      « aqui: referências anuláveis ligadas
  </PropertyGroup>

</Project>

Com dotnet run arquivo.cs, que você usa nesta parte do guia, o sistema também vem ligado, sem precisar de nada. O editor do guia faz o mesmo. Projetos antigos (antes do .NET 6) e muitos exemplos da internet não têm essa linha; neles, nada disso aparece. O arquivo .csproj é explicado em O Arquivo .csproj.


string × string?

string nome = null;
string? apelido = null;
Console.WriteLine($"[{nome}] [{apelido}]");
$ dotnet run r1.cs
/home/ana/cs/r1.cs(1,15): warning CS8600: Conversão de literal nula ou possível valor nulo em tipo não anulável.
[] []
string nome = null;
string sem ? promete "sempre tem texto". Pôr null nela quebra a promessa, e o compilador avisa na linha 1, coluna 15 (onde está o null).
warning CS8600
"Conversão de um null (ou de algo que pode ser null) para um tipo que não aceita nulo". Em inglês: Converting null literal or possible null value to non-nullable type. É um aviso: o programa rodou mesmo assim.
string? apelido = null;
Com ?, a caixa tem a etiqueta "pode estar vazia". Nenhum aviso.
[] []
As duas variáveis valem null e aparecem em branco. O aviso não mudou nada no programa.

A regra para escolher é sempre a mesma pergunta: "faz sentido isto estar em branco?" O nome de um aluno, não: use string. O apelido, sim: use string?.


O compilador acompanha o seu código

O compilador não olha só a declaração. Ele segue o programa linha por linha, e sabe, em cada ponto, se a variável pode ser null ali. Isso se chama análise de fluxo:

string? nome = "Bia";
Console.WriteLine(nome.Length);
nome = null;
Console.WriteLine(nome.Length);
$ dotnet run r11.cs; echo "código: $?"
/home/ana/cs/r11.cs(4,19): warning CS8602: Desreferência de uma referência possivelmente nula.
3
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/r11.cs:line 4
código: 134

Repare: a variável é string? nas duas linhas, mas só a linha 4 ganhou aviso. Na linha 2, o compilador sabe que nome acabou de receber "Bia". Na linha 4, ele sabe que acabou de receber null. E ele estava certo: foi exatamente na linha 4 que o programa quebrou.

O CS8602 é o aviso mais comum: "desreferência de uma referência possivelmente nula". "Desreferenciar" é usar o ponto (.Length, .ToUpper()) para ir até o objeto que o endereço aponta. Em inglês: Dereference of a possibly null reference.

Conferir antes tira o aviso

O jeito certo de usar um string? é conferir antes. O compilador entende o if:

string? nome = Console.ReadLine();
if (nome is null)
{
    Console.WriteLine("Nada foi digitado.");
    return;
}
Console.WriteLine($"{nome} tem {nome.Length} letras.");
$ echo Bia | dotnet run r4.cs
Bia tem 3 letras.
$ dotnet run r4.cs < /dev/null       « entrada vazia: ReadLine devolve null
Nada foi digitado.
string? nome = Console.ReadLine();
ReadLine devolve string?, porque pode não haver nada para ler. Guardar em string? é honesto e não dá aviso.
if (nome is null) { … return; }
Se estiver em branco, avisa e sai do programa (o return, nas instruções de nível superior, encerra o programa).
nome.Length
Nenhum aviso aqui: o compilador sabe que, se o programa chegou nesta linha, nome não é null.

O compilador entende várias formas de conferir, mas só dentro do lugar que a conferência protege:

string? nome = Console.ReadLine();
if (!string.IsNullOrEmpty(nome))
    Console.WriteLine(nome.Length);
if (nome != null)
    Console.WriteLine(nome.ToUpper());
Console.WriteLine(nome.Length);
$ echo Bia | dotnet run r5.cs
/home/ana/cs/r5.cs(6,19): warning CS8602: Desreferência de uma referência possivelmente nula.
3
BIA
3

As linhas 3 e 5 estão protegidas pelos if (o string.IsNullOrEmpty, "é nulo ou vazio?", também conta). A linha 6 está fora deles: lá, nome ainda pode ser null, e o aviso aparece. Desta vez a Bia digitou o nome e o programa não quebrou, mas com uma entrada vazia ele quebraria.


Avisos com métodos

Os parâmetros e o retorno de um método também dizem se aceitam null. Um método Saudar(string nome) promete que nome sempre tem texto, então ele usa nome.Length sem conferir. Quem chama precisa cumprir a promessa:

void Saudar(string nome)
{
    Console.WriteLine($"Olá, {nome}! Seu nome tem {nome.Length} letras.");
}

string? Buscar(int codigo)
{
    if (codigo == 1) return "Ana";
    return null;
}

string? quem = Buscar(2);
Saudar(quem);
$ dotnet run r2.cs; echo "código: $?"
/home/ana/cs/r2.cs(13,8): warning CS8604: Possível argumento de referência nula para o parâmetro 'nome' em 'void Saudar(string nome)'.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<<Main>$>g__Saudar|0_0(String nome) in /home/ana/cs/r2.cs:line 3
   at Program.<Main>$(String[] args) in /home/ana/cs/r2.cs:line 13
código: 134
string? Buscar(int codigo)
O retorno string? avisa: "posso não encontrar ninguém". Com o código 2, devolve null.
warning CS8604
"Possível argumento nulo para o parâmetro nome". A variável quem pode ser null, e o parâmetro de Saudar não aceita. O aviso aponta a linha 13, a da chamada.
g__Saudar|0_0 … line 3
A exceção mostra o caminho: quebrou na linha 3, dentro do Saudar (o nome estranho é como o C# chama, por dentro, um método escrito no meio das instruções de nível superior), que foi chamado da linha 13. Leia a pilha de baixo para cima, como em Exceções.

O aviso aponta a chamada (linha 13), e não o nome.Length da linha 3. Isso faz sentido: o erro não é de Saudar, que confiou na promessa; é de quem mandou um null.

Missão: rode e leia o aviso CS8604. Conserte só a última linha para que, quando a busca não encontrar ninguém, o Saudar receba "visitante" (dica: ??, da aula anterior). A saída deve começar com Olá, visitante!, sem nenhum aviso.

void Saudar(string nome)
{
    Console.WriteLine($"Olá, {nome}! Seu nome tem {nome.Length} letras.");
}

string? Buscar(int codigo)
{
    if (codigo == 1) return "Ana";
    return null;
}

string? quem = Buscar(2);
Saudar(quem);
void Saudar(string nome)
{
    Console.WriteLine($"Olá, {nome}! Seu nome tem {nome.Length} letras.");
}

string? Buscar(int codigo)
{
    if (codigo == 1) return "Ana";
    return null;
}

string? quem = Buscar(2);
Saudar(quem ?? "visitante");

Quando o próprio método quebra a promessa

Se o retorno é string (sem ?) e o método devolve null, o aviso é outro:

string Buscar(int codigo)
{
    if (codigo == 1) return "Ana";
    return null;
}
Console.WriteLine(Buscar(1));
$ dotnet run r3.cs
/home/ana/cs/r3.cs(4,12): warning CS8603: Possível retorno de referência nula.
Ana

O CS8603 ("possível retorno nulo") aponta o return null;. O conserto é ser honesto no tipo: string? Buscar(int codigo). Aí quem chama passa a saber que precisa tratar o "não encontrei".

E se você mandar o null escrito direto para um parâmetro string, o aviso é o CS8625:

$ dotnet run r10.cs
/home/ana/cs/r10.cs(5,8): warning CS8625: Não é possível converter um literal nulo em um tipo de referência não anulável.
Olá, !

O operador !: "confie em mim"

Às vezes você sabe algo que o compilador não sabe, e quer calar o aviso. Para isso existe o ! depois da variável (o "operador que perdoa nulo", null-forgiving): nome!.Length quer dizer "eu garanto que nome não é null aqui".

string? nome = null;
Console.WriteLine(nome!.Length);
$ dotnet run r6.cs; echo "código: $?"
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/r6.cs:line 2
código: 134

O aviso sumiu, mas a exceção não. O ! não confere nada e não muda o programa: ele só manda o compilador ficar quieto. Aqui a "garantia" era falsa, e o programa quebrou sem nenhum aviso antes.

Regra prática

Não use ! para fazer um aviso sumir. Primeiro pergunte "isto pode mesmo ser null aqui?". Se pode, trate com if, ?? ou ?.. Se não pode, normalmente o tipo é que está errado: troque string? por string. O ! fica para os raros casos em que você tem certeza e não há como o compilador saber.

Não confunda com o ! do "não" lógico (!pronto, de Operadores Lógicos). O "não" vem antes; o "confie em mim" vem depois da variável.


Desligar e endurecer

#nullable disable: o jeito antigo

A linha #nullable disable no topo do arquivo desliga o sistema, como era antes do C# 8. Veja o que acontece:

#nullable disable
string nome = null;
string? apelido = null;
Console.WriteLine(nome.Length);
$ dotnet run r7.cs; echo "código: $?"
/home/ana/cs/r7.cs(3,7): warning CS8632: A anotação para tipos de referência anuláveis deve ser usada apenas em código em um contexto de anotações '#nullable'.
/home/ana/cs/r7.cs(3,9): warning CS0219: A variável "apelido" é atribuída, mas seu valor nunca é usado
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/r7.cs:line 4
código: 134

Nenhum aviso sobre o null da linha 2 nem sobre o nome.Length da linha 4, e o programa quebrou do mesmo jeito. O único aviso de nulidade é o CS8632: o ? de string? não significa nada com o sistema desligado. Num projeto, o mesmo efeito vem de <Nullable>disable</Nullable>. Você vai encontrar código assim em projetos antigos; nos seus, deixe ligado.

WarningsAsErrors: aviso vira erro

No outro extremo, você pode pedir que os avisos de nulidade sejam tratados como erros: o programa nem compila enquanto houver um. Com dotnet run arquivo.cs, isso é uma linha #:property no topo; num projeto, é <WarningsAsErrors>nullable</WarningsAsErrors> no .csproj.

#:property WarningsAsErrors=nullable
string? nome = Console.ReadLine();
Console.WriteLine(nome.Length);
$ dotnet run r8.cs < /dev/null; echo "código: $?"
/home/ana/cs/r8.cs(3,19): error CS8602: Desreferência de uma referência possivelmente nula.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
código: 1

O mesmo CS8602, agora como error. O programa não rodou, então não teve como quebrar. Muitas equipes trabalham assim.


Formas diferentes e qual usar

SituaçãoO que fazer
O valor nunca pode faltar (nome do aluno)Declare string e garanta um texto sempre.
O valor pode faltar (apelido, resultado de uma busca)Declare string?, e confira antes de usar o ponto.
Faltou? Use um padrãoapelido ?? "sem apelido"
Faltou? Faça outra coisaif (nome is null) { …; return; } e, depois, use à vontade
Só quer um pedaço, se existirnome?.Length (o resultado é anulável)
Tem certeza absoluta e o compilador não tem como sabernome!, raramente

Quando dá errado

Os avisos de nulidade começam todos com CS86. Esta tabela traduz cada um e dá o conserto mais comum:

AvisoMensagem realQuando apareceConserto típico
CS8600Conversão de literal nula ou possível valor nulo em tipo não anulável.string s = null; ou string s = Console.ReadLine();string?, ou ?? "padrão"
CS8602Desreferência de uma referência possivelmente nula.s.Length com s talvez nullif (s is not null), s?.Length
CS8603Possível retorno de referência nula.return null; num método que devolve stringretorno string?
CS8604Possível argumento de referência nula para o parâmetro…mandar um string? para um parâmetro stringMetodo(s ?? "padrão"), ou conferir antes
CS8625Não é possível converter um literal nulo em um tipo de referência não anulável.Metodo(null) com parâmetro stringparâmetro string?, ou mandar um texto
CS8632A anotação para tipos de referência anuláveis deve ser usada apenas em código em um contexto de anotações '#nullable'.string? com o sistema desligadoligar o sistema (<Nullable>enable</Nullable>)
"Mas o programa rodou!" (e quebrou depois)
$ dotnet run r11.cs; echo "código: $?"
/home/ana/cs/r11.cs(4,19): warning CS8602: Desreferência de uma referência possivelmente nula.
3
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/r11.cs:line 4
código: 134

O que aconteceu: o aviso apareceu, o programa rodou porque avisos não impedem a compilação, e quebrou exatamente na linha que o aviso apontava.

Como resolver: trate cada CS86xx como um erro de verdade. Se quiser que o compilador obrigue, use WarningsAsErrors=nullable.

O ! escondeu o problema
$ dotnet run r6.cs; echo "código: $?"
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/r6.cs:line 2
código: 134

O que aconteceu: nome!.Length calou o CS8602, mas nome era mesmo null.

Como resolver: apague o !, leia o aviso de volta e trate o null de verdade: nome?.Length ?? 0 ou um if.

O aviso aponta para um lugar, o problema está em outro
$ dotnet run r2.cs
/home/ana/cs/r2.cs(13,8): warning CS8604: Possível argumento de referência nula para o parâmetro 'nome' em 'void Saudar(string nome)'.

O que aconteceu: o aviso está na chamada (linha 13), mas a "culpa" pode ser do Buscar, que devolve string?.

Como resolver: pergunte onde o null nasce. Se a busca pode mesmo falhar, trate na chamada (quem ?? "visitante"). Se nunca deveria falhar, mude o método.


Referência

EscritaSignificado
stringSempre tem texto (com o sistema ligado, o compilador cobra)
string?Pode ser null; confira antes de usar o ponto
string?[]Um array (que existe) de textos que podem ser null
string[]?Um array que pode não existir, de textos que existem
x!"Confie em mim": cala o aviso, não confere nada
<Nullable>enable</Nullable>Liga o sistema no projeto (padrão desde o .NET 6)
#nullable disable / #nullable enableDesliga / liga a partir desta linha do arquivo
#:property WarningsAsErrors=nullableAvisos de nulidade viram erros (no dotnet run arquivo.cs)
<WarningsAsErrors>nullable</WarningsAsErrors>O mesmo, num projeto

Missão final: contar letras sem medo

Missão: o método ContarLetras aceita string?, mas usa texto.Length sem conferir. Rode: aparece o aviso CS8602 e o programa quebra no nome em branco. Conserte só o método para que um texto null conte 0 letras. A saída deve ser Total de letras: 7, sem nenhum aviso.

int ContarLetras(string? texto)
{
    return texto.Length;
}

string?[] nomes = { "ana", null, "caio" };
int total = 0;
foreach (string? n in nomes)
    total += ContarLetras(n);
Console.WriteLine($"Total de letras: {total}");
int ContarLetras(string? texto)
{
    if (texto is null)
        return 0;
    return texto.Length;
}

string?[] nomes = { "ana", null, "caio" };
int total = 0;
foreach (string? n in nomes)
    total += ContarLetras(n);
Console.WriteLine($"Total de letras: {total}");

Exercícios

1. Pode estar em branco

Complete para declarar apelido como um texto que pode ficar em branco:

apelido = null;

2. O aviso mais comum

Qual é o código do aviso "Desreferência de uma referência possivelmente nula"?

3. Desligar no arquivo

Complete a linha que desliga as referências anuláveis a partir do topo do arquivo:

#nullable

4. Um programa tem um aviso CS8602. O que acontece com dotnet run?

5. O que nome!.Length faz quando nome é null?

6. string? Buscar(int codigo): o que o ? no retorno comunica?


Resumo

  • Com as referências anuláveis ligadas (padrão no dotnet new console e no dotnet run arquivo.cs), string promete "sempre tem texto" e string? avisa "pode estar em branco".
  • São avisos, não erros: o programa roda e pode quebrar. Trate cada CS86xx como erro, ou use WarningsAsErrors=nullable.
  • O compilador acompanha o fluxo: depois de if (x is null) return; ou dentro de if (x is not null), o ponto é seguro.
  • Avisos principais: CS8600 (pôr nulo em não anulável), CS8602 (usar o ponto em talvez-nulo), CS8603 (retornar nulo), CS8604/CS8625 (mandar nulo para parâmetro), CS8632 (? com o sistema desligado).
  • O ! só cala o aviso; não evita a exceção. Use raramente.
  • Próxima parte do guia: O que é POO, o começo das classes e objetos. Cada objeto que você criar também será um tipo de referência.