C# null e Tipos Anuláveis
Nem toda informação existe o tempo todo. O aluno que faltou à prova não tem nota, e isso é bem diferente de ter tirado zero. A pessoa que não quis dizer a idade não tem idade "0". Em C#, a ausência de valor tem nome: null (lê-se "nul", e quer dizer "nulo", "nada"). Nesta aula você vai ver por que um int comum não aceita null, como o int? resolve isso e quais erros aparecem quando o "nada" é usado como se fosse um valor.
- O que é
nulle por queint idade = null;não compila (erroCS0037). - Os tipos anuláveis:
int?,double?,bool?,decimal?e o nome completoNullable<int>. - Como perguntar se há valor:
HasValue,== null,is null,is not nulleis int n. - Como pegar o valor:
Value,GetValueOrDefault()e o cast(int), e qual deles nunca quebra. - O que acontece numa conta ou comparação com
null. - Que textos (
string) também podem sernull, e a famosaNullReferenceException. - Os erros reais:
CS0037,CS0266,CS1061,InvalidOperationExceptioneNullReferenceException.
Todos os programas desta aula rodaram com dotnet run arquivo.cs (veja Rodar com dotnet run) na máquina fictícia da Ana (usuária ana, computador notebook), com o .NET 10 SDK, na pasta ~/cs. Você pode repetir cada um no editor do guia, que compila C# de verdade no navegador.
A ideia antes do código: o campo em branco
Imagine a planilha de notas da professora da Ana. Ao lado do nome da Bia está escrito 0: ela fez a prova e errou tudo. Ao lado do nome do Caio, o campo está em branco: ele faltou e ainda vai fazer a prova de recuperação.
Se a professora escrevesse 0 para o Caio, a média da turma ficaria errada, e o Caio ficaria com uma nota que nunca tirou. O campo em branco carrega uma informação importante: "ainda não há valor aqui".
Em C#, esse campo em branco é o null. Ele não é zero, não é texto vazio, não é false: é a falta de qualquer valor.
int não aceita null
Os tipos que você usou até aqui (int, double, decimal, bool, char) são tipos de valor: a variável é uma caixa que sempre guarda um número, um true/false ou uma letra. Ela nunca fica "vazia". Por isso, o compilador recusa isto:
int idade = null; Console.WriteLine(idade);
$ dotnet run erro1.cs /home/ana/cs/erro1.cs(1,13): error CS0037: Não é possível converter o valor nulo em 'int' porque ele não é um tipo de valor não anulável Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
A tradução da mensagem é confusa (tem um "não" a mais). Em inglês ela é mais clara: Cannot convert null to 'int' because it is a non-nullable value type, ou seja, "não dá para pôr null num int, porque int é um tipo de valor que não aceita nulo". O (1,13) aponta a linha 1, coluna 13: exatamente onde está o null.
O int?: um int que pode ficar em branco
Basta um ponto de interrogação depois do tipo. int? (lê-se "int anulável") é um tipo que guarda ou um número inteiro ou null:
int? idade = null;
Console.WriteLine($"Idade: [{idade}]");
Console.WriteLine(idade.HasValue);
idade = 29;
Console.WriteLine($"Idade: [{idade}]");
Console.WriteLine(idade.HasValue);
Console.WriteLine(idade.Value);
$ dotnet run nulo.cs Idade: [] « null aparece como nada: os colchetes ficaram vazios False « ainda não tem valor Idade: [29] True « agora tem 29
- int? idade = null;
- Cria a variável
idadedo tipoint?e começa em branco. O?faz parte do tipo:int?eintsão tipos diferentes. - [{idade}]
- Os colchetes são só texto, para você enxergar onde o valor deveria estar. Ao mostrar um
null, o C# escreve nada: nem "null", nem "0". - idade.HasValue
- "Tem valor?" (has value, em inglês). É uma propriedade de todo tipo anulável: vale
truequando há um número efalsequando énull. - idade = 29;
- Um
intcomum entra numint?sem cerimônia. A conversão deintparaint?é automática, porque nunca perde nada. - idade.Value
- "O valor": devolve o
intque está guardado. Só funciona quando há valor; comnull, o programa quebra (você verá isso daqui a pouco).
Qualquer tipo de valor ganha o seu ?
| Tipo | Guarda | Exemplo do dia a dia |
|---|---|---|
int? | um inteiro ou null | a nota de uma prova que talvez ainda não foi feita |
double? | um número com vírgula ou null | a altura que a pessoa não informou |
decimal? | um valor em dinheiro ou null | o preço de um produto que ainda não foi cadastrado |
bool? | true, false ou null | "aceitou os termos?": sim, não ou ainda não respondeu |
char? | uma letra ou null | a turma ("A", "B"…) de um aluno ainda não matriculado |
Nullable<int>
int? é um apelido curto. O nome de verdade do tipo é Nullable<int> ("anulável de int"). As duas linhas abaixo são idênticas para o compilador:
int? a = 5; Nullable<int> b = 5;
Use sempre a forma curta, int?. Mas reconheça a longa: ela aparece nas mensagens de exceção, como System.Nullable`1.get_Value(), que você verá mais abaixo. Os sinais < > são assunto de Generics.
Um array de anuláveis começa em branco
Um new int[3] nasce cheio de zeros. Um new int?[3] nasce cheio de null: o "valor padrão" de um tipo anulável é null.
int?[] notas = new int?[3];
notas[1] = 9;
for (int i = 0; i < notas.Length; i++)
Console.WriteLine($"notas[{i}] = [{notas[i]}]");
int[] zeros = new int[3];
Console.WriteLine(zeros[0]);
$ dotnet run arr.cs notas[0] = [] notas[1] = [9] notas[2] = [] 0 « o int comum começa em 0
Perguntar se há valor
Antes de usar um anulável, pergunte se ele tem valor. A planilha da professora vira código assim:
int? notaBia = 0;
int? notaCaio = null;
Console.WriteLine($"Bia: [{notaBia}] tem nota? {notaBia.HasValue}");
Console.WriteLine($"Caio: [{notaCaio}] tem nota? {notaCaio.HasValue}");
if (notaCaio == null)
Console.WriteLine("O Caio ainda não fez a prova.");
if (notaBia is not null)
Console.WriteLine($"A Bia tirou {notaBia.Value}.");
$ dotnet run fichas.cs Bia: [0] tem nota? True « zero É um valor Caio: [] tem nota? False O Caio ainda não fez a prova. A Bia tirou 0.
Repare na primeira linha da saída: a nota 0 da Bia tem valor. É exatamente a diferença da planilha: 0 é uma nota, null é a falta dela. Há quatro jeitos de perguntar, e todos dão a mesma resposta:
| Escrita | Lê-se | Verdade quando… |
|---|---|---|
nota.HasValue | "nota tem valor" | há um número |
nota != null | "nota é diferente de nulo" | há um número |
nota is not null | "nota não é nulo" | há um número |
nota == null ou nota is null | "nota é nulo" | está em branco |
is int n: perguntar e pegar de uma vez
Muitas vezes você pergunta "tem valor?" só para, na linha seguinte, pegar o valor. O C# junta as duas coisas:
if (notaBia is int n)
Console.WriteLine($"A Bia tirou {n}.");
Leia como: "se notaBia é um int, chame esse int de n". Quando há valor, a condição é verdadeira e a variável n (um int comum, sem ?) já nasce com o número. Quando é null, a condição é falsa e o bloco não roda. Esse recurso se chama pattern matching e tem uma aula inteira: Pattern Matching.
Brinque com o código: troque a nota do Caio para 7 e rode de novo. Depois troque a da Bia para null. O que muda em cada linha? Tente também trocar notaBia.Value por notaBia e veja que o resultado é o mesmo dentro do $"…".
int? notaBia = 0;
int? notaCaio = null;
Console.WriteLine($"Bia: [{notaBia}] tem nota? {notaBia.HasValue}");
Console.WriteLine($"Caio: [{notaCaio}] tem nota? {notaCaio.HasValue}");
if (notaCaio == null)
Console.WriteLine("O Caio ainda não fez a prova.");
if (notaBia is int n)
Console.WriteLine($"A Bia tirou {n}.");
Pegar o valor: três caminhos
Um int? não entra direto numa variável int. O compilador não deixa, porque o int não teria onde pôr o null:
int? nota = 7; int total = nota; Console.WriteLine(total);
$ dotnet run erro2.cs /home/ana/cs/erro2.cs(2,13): error CS0266: Não é possível converter implicitamente tipo "int?" em "int". Existe uma conversão explícita (há uma conversão ausente?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Mesmo que, nesse caso, a nota seja 7: o compilador olha o tipo, e o tipo int? pode ser null. Você precisa dizer o que fazer. Os três caminhos:
int? bonus = null; int? nota = 7; Console.WriteLine(nota.Value); Console.WriteLine((int)nota); Console.WriteLine(bonus.GetValueOrDefault()); Console.WriteLine(bonus.GetValueOrDefault(10)); Console.WriteLine(nota.GetValueOrDefault(10));
$ dotnet run valor.cs 7 7 0 « bonus é null: veio o padrão do int, 0 10 « bonus é null: veio o padrão que você escolheu 7 « nota tem valor: o 10 foi ignorado
- nota.Value
- Devolve o
intguardado. Se fornull, o programa quebra comInvalidOperationException. - (int)nota
- Um cast: "eu garanto que isto é um
int". É o mesmo que.Value, inclusive na quebra. É a "conversão explícita" de que a mensagem doCS0266fala. - bonus.GetValueOrDefault()
- "Pegue o valor ou o padrão". Com valor, devolve o valor; com
null, devolve o padrão do tipo (0 paraint,falseparabool). Nunca quebra. - bonus.GetValueOrDefault(10)
- O mesmo, mas com o padrão que você escolhe entre os parênteses.
Se você acabou de conferir que há valor (com if (nota.HasValue)), .Value é seguro. Se não conferiu, prefira GetValueOrDefault(…) ou o operador ??, que faz a mesma coisa com menos letras: bonus ?? 10. Ele é o assunto da próxima aula, Operadores ??, ?. e ??=.
Missão: este programa não compila. Rode e leia o erro: pontos + bonus dá um int?, que não cabe em int total. Conserte a linha 3 usando GetValueOrDefault() no bonus, para que um bônus em branco valha 0. A saída deve ser Total: 50.
int? bonus = null;
int pontos = 50;
int total = pontos + bonus;
Console.WriteLine($"Total: {total}");
int? bonus = null;
int pontos = 50;
int total = pontos + bonus.GetValueOrDefault();
Console.WriteLine($"Total: {total}");
Contas e comparações com null
O que dá 7 + nada? Para o C#, dá nada. Se um dos lados de uma conta é null, o resultado também é null. E uma comparação como > ou < com null é sempre false, porque não dá para dizer se "nada" é maior ou menor que 5:
int? bonus = null;
int? nota = 7;
Console.WriteLine($"[{nota + bonus}]");
Console.WriteLine($"[{nota + 1}]");
Console.WriteLine(bonus > 5);
Console.WriteLine(bonus < 5);
Console.WriteLine(bonus == null);
$ dotnet run contas.cs [] « 7 + null = null [8] « 7 + 1 = 8 False « null > 5? não False « null < 5? também não! True
Com números comuns, se x > 5 é falso, você pensa "então x <= 5". Com null, os dois são falsos. Um if (nota >= 6) … else Console.WriteLine("Reprovado"); vai dizer "Reprovado" para quem ainda não fez a prova. Trate o null antes, com if (nota is null).
Um método que pode não ter resposta
Um bom uso do int? é como retorno de um método que às vezes não tem o que devolver. Este método tenta transformar um texto em número com int.TryParse e devolve null quando o texto não é um número:
int? LerNumero(string texto)
{
if (int.TryParse(texto, out int numero))
return numero;
return null;
}
int? a = LerNumero("42");
int? b = LerNumero("quarenta");
Console.WriteLine($"a = [{a}], tem valor? {a.HasValue}");
Console.WriteLine($"b = [{b}], tem valor? {b.HasValue}");
$ dotnet run ler.cs a = [42], tem valor? True b = [], tem valor? False
- int? LerNumero(string texto)
- O tipo de retorno é
int?: o método promete devolver um número ou nada. Quem chama já sabe, pelo tipo, que precisa tratar o "nada". - return numero;
- Um
intcomum viraint?automaticamente na volta. - return null;
- Só é permitido porque o retorno é
int?. Com retornoint, seria o erroCS0037.
Compare com devolver 0 ou -1 para dizer "não deu": e se a pessoa digitou mesmo 0? Com int?, o "não deu" nunca se confunde com um número de verdade.
Textos também podem ser null
Uma string não é um tipo de valor: é um tipo de referência (a diferença está em Valor × Referência). A variável não guarda o texto em si, e sim o "endereço" de onde ele está. E um endereço pode ficar em branco: uma string pode valer null, que é diferente de "" (um texto vazio, mas que existe).
O perigo aparece quando você pede algo a um texto que não existe, como o tamanho dele:
string? nome = null;
Console.WriteLine($"Nome: [{nome}]");
Console.WriteLine(nome.Length);
$ dotnet run nre2.cs; echo "código: $?" /home/ana/cs/nre2.cs(3,19): warning CS8602: Desreferência de uma referência possivelmente nula. Nome: [] Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in /home/ana/cs/nre2.cs:line 3 código: 134
- warning CS8602
- Antes de rodar, o compilador já avisou: "na linha 3, coluna 19, você usa algo que pode ser nulo". É um aviso, não um erro, então o programa roda mesmo assim.
- Nome: []
- Mostrar um
nullnão quebra nada: sai em branco. - System.NullReferenceException
- "Exceção de referência nula". A mensagem
Object reference not set to an instance of an objectquer dizer "a referência não aponta para objeto nenhum". Não há texto, logo não há.Length. - line 3
- A linha que quebrou. O programa parou ali.
- código: 134
- O código de saída de um programa que morreu por exceção não tratada, no Linux.
O ? em string? e o aviso CS8602 fazem parte do sistema de referências anuláveis, que ajuda o compilador a encontrar esses erros antes de o programa rodar. Ele tem uma aula própria: Referências Anuláveis (string?). Por enquanto, guarde a regra: nunca use . num valor que pode ser null sem conferir antes.
Formas diferentes e qual usar
| Você quer… | Forma | Quando usar |
|---|---|---|
| saber se há valor | x.HasValue, x != null, x is not null | As três são iguais. is not null lê como português e também serve para string. |
| saber e pegar o valor | if (x is int n) | Quando vai usar o número logo em seguida. O n é um int comum. |
| pegar o valor sabendo que existe | x.Value ou (int)x | Só depois de conferir. Sem conferir, pode dar InvalidOperationException. |
| pegar o valor ou um padrão | x.GetValueOrDefault(10) ou x ?? 10 | Quando o branco tem um substituto óbvio. O ?? é o mais usado (próxima aula). |
Quando dá errado
null num tipo que não aceita
$ dotnet run erro1.cs /home/ana/cs/erro1.cs(1,13): error CS0037: Não é possível converter o valor nulo em 'int' porque ele não é um tipo de valor não anulável Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: int idade = null;. Um int sempre tem um número; não há lugar para "nada".
Como resolver: se o branco faz sentido, use int? idade = null;. Se não faz, dê um número de verdade.
int? não cabe em int
$ dotnet run erro2.cs /home/ana/cs/erro2.cs(2,13): error CS0266: Não é possível converter implicitamente tipo "int?" em "int". Existe uma conversão explícita (há uma conversão ausente?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: int total = nota;, com nota do tipo int?. O mesmo erro aparece em int total = pontos + bonus;: a soma com um int? também é int?.
Como resolver: decida o que fazer com o branco: nota.GetValueOrDefault() (ou nota ?? 0), ou confira antes e use nota.Value. Mudar total para int? também resolve, se o total puder ficar em branco.
Nullable object must have a value
int? nota = null; int total = nota.Value; Console.WriteLine(total);
$ dotnet run erro3.cs; echo "código: $?" /home/ana/cs/erro3.cs(2,13): warning CS8629: O tipo de valor de nulidade pode ser nulo. Unhandled exception. System.InvalidOperationException: Nullable object must have a value. at System.Nullable`1.get_Value() at Program.<Main>$(String[] args) in /home/ana/cs/erro3.cs:line 2 código: 134
O que aconteceu: o código pediu o .Value de um anulável vazio. A exceção diz "o objeto anulável precisa ter um valor". A linha System.Nullable`1.get_Value() é o nome interno da propriedade Value do Nullable<int>. O cast (int)nota quebra do mesmo jeito. O compilador até avisou antes, com o CS8629 ("o tipo de valor anulável pode ser nulo"; a tradução oficial é estranha mesmo).
Como resolver: confira antes (if (nota.HasValue) ou if (nota is int n)) ou use nota.GetValueOrDefault(). Leve os avisos a sério: este estava certo.
NullReferenceException: usar um texto que não existe
$ dotnet run nre2.cs; echo "código: $?" /home/ana/cs/nre2.cs(3,19): warning CS8602: Desreferência de uma referência possivelmente nula. Nome: [] Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in /home/ana/cs/nre2.cs:line 3 código: 134
O que aconteceu: nome.Length com nome valendo null. É a exceção mais comum do C#: qualquer . depois de algo nulo (.Length, .ToUpper(), .Nome…) a provoca.
Como resolver: confira antes (if (nome is not null)) ou use os operadores da próxima aula, como nome?.Length. E preste atenção no aviso CS8602: ele apontou a linha exata antes de o programa rodar.
HasValue num int comum
$ dotnet run e4.cs /home/ana/cs/e4.cs(2,25): error CS1061: ‘int’ não contém uma definição para "HasValue" e não foi possível encontrar nenhum método de extensão "HasValue" que aceite um primeiro argumento do tipo ‘int’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: int idade = 29; Console.WriteLine(idade.HasValue);. HasValue, Value e GetValueOrDefault só existem nos tipos anuláveis. Um int comum sempre tem valor, então não tem o que perguntar.
Como resolver: se a variável pode ficar em branco, declare-a como int?. Se não pode, apague a pergunta.
Referência
| Escrita | Significado |
|---|---|
null | Nenhum valor. Diferente de 0, de "" e de false. |
int?, double?, bool?… | Tipo anulável: o tipo de valor ou null. O mesmo que Nullable<int>. |
x.HasValue | true se há valor |
x == null, x is null | true se está em branco |
x != null, x is not null | true se há valor |
x is int n | true se há valor, e o põe em n |
x.Value, (int)x | O valor; InvalidOperationException se for null |
x.GetValueOrDefault() | O valor, ou o padrão do tipo (0, false…) |
x.GetValueOrDefault(p) | O valor, ou p |
x + y com um null | null |
x > y, x < y com um null | false |
new int?[n] | Array com n posições, todas null |
Missão final: a média de quem fez a prova
Missão: a turma tem 5 alunos, mas 2 faltaram (null). O programa trata o branco como 0 e divide por 5, e a média sai 4,8: errada! Mude o laço para somar e contar só as notas que existem (use if (nota.HasValue) ou if (nota is int n)). A saída deve ser Provas feitas: 3 e Média: 8.
int?[] notas = { 8, null, 6, null, 10 };
double soma = 0;
int feitas = 0;
foreach (int? nota in notas)
{
soma += nota.GetValueOrDefault();
feitas++;
}
Console.WriteLine($"Provas feitas: {feitas}");
Console.WriteLine($"Média: {soma / feitas}");
int?[] notas = { 8, null, 6, null, 10 };
double soma = 0;
int feitas = 0;
foreach (int? nota in notas)
{
if (nota.HasValue)
{
soma += nota.Value;
feitas++;
}
}
Console.WriteLine($"Provas feitas: {feitas}");
Console.WriteLine($"Média: {soma / feitas}");
Exercícios
1. Declarar um anulável
Complete para criar a variável altura, um double que pode ficar em branco, começando sem valor:
2. Tem valor?
Complete com a propriedade que diz se nota tem valor:
3. Valor ou padrão
Complete para que total receba o bônus, ou 5 se o bônus estiver em branco, sem nunca quebrar:
4. O que mostra int? x = null; Console.WriteLine($"[{x}]");?
5. Com int? nota = null;, quanto vale nota < 6?
6. Qual destas linhas pode quebrar o programa com InvalidOperationException quando nota é null?
Resumo
nullé a falta de valor: não é 0, nem"", nemfalse.- Tipos de valor (
int,double,bool…) não aceitamnull: erroCS0037. Com?(int?, o mesmo queNullable<int>), aceitam. - Pergunte antes de usar:
HasValue,!= null,is not null, ou pergunte e pegue comis int n. int?não entra emint(CS0266). Pegue o valor com.Value/(int)(quebram comnull:InvalidOperationException) ou comGetValueOrDefault(…), que nunca quebra.- Conta com
nulldánull;>e<comnulldãofalse. stringtambém pode sernull. Usar.numnulldáNullReferenceException, código de saída 134. O avisoCS8602avisa antes.- Próxima aula:
??,?.e??=, os atalhos para lidar comnullem uma linha.