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.
- A diferença entre
string("sempre tem texto") estring?("pode estar em branco"). - Onde o sistema é ligado: o
<Nullable>enable</Nullable>do projeto e odotnet run arquivo.cs. - Como o compilador "acompanha" o código: depois de um
if (nome is null) return;, ele sabe quenometem texto. - Cada aviso real:
CS8600,CS8602,CS8603,CS8604,CS8625eCS8632. - 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.
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;
stringsem?promete "sempre tem texto". Pôrnullnela quebra a promessa, e o compilador avisa na linha 1, coluna 15 (onde está onull).- warning CS8600
- "Conversão de um
null(ou de algo que pode sernull) 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
nulle 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();
ReadLinedevolvestring?, porque pode não haver nada para ler. Guardar emstring?é 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,
nomenã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, devolvenull. - warning CS8604
- "Possível argumento nulo para o parâmetro
nome". A variávelquempode sernull, e o parâmetro deSaudarnã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.
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ção | O 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ão | apelido ?? "sem apelido" |
| Faltou? Faça outra coisa | if (nome is null) { …; return; } e, depois, use à vontade |
| Só quer um pedaço, se existir | nome?.Length (o resultado é anulável) |
| Tem certeza absoluta e o compilador não tem como saber | nome!, raramente |
Quando dá errado
Os avisos de nulidade começam todos com CS86. Esta tabela traduz cada um e dá o conserto mais comum:
| Aviso | Mensagem real | Quando aparece | Conserto típico |
|---|---|---|---|
CS8600 | Conversã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" |
CS8602 | Desreferência de uma referência possivelmente nula. | s.Length com s talvez null | if (s is not null), s?.Length |
CS8603 | Possível retorno de referência nula. | return null; num método que devolve string | retorno string? |
CS8604 | Possível argumento de referência nula para o parâmetro… | mandar um string? para um parâmetro string | Metodo(s ?? "padrão"), ou conferir antes |
CS8625 | Não é possível converter um literal nulo em um tipo de referência não anulável. | Metodo(null) com parâmetro string | parâmetro string?, ou mandar um texto |
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'. | string? com o sistema desligado | ligar o sistema (<Nullable>enable</Nullable>) |
$ 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.
! 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.
$ 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
| Escrita | Significado |
|---|---|
string | Sempre 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 enable | Desliga / liga a partir desta linha do arquivo |
#:property WarningsAsErrors=nullable | Avisos 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:
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:
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 consolee nodotnet run arquivo.cs),stringpromete "sempre tem texto" estring?avisa "pode estar em branco". - São avisos, não erros: o programa roda e pode quebrar. Trate cada
CS86xxcomo erro, ou useWarningsAsErrors=nullable. - O compilador acompanha o fluxo: depois de
if (x is null) return;ou dentro deif (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.