C# Operadores ??, ?. e ??=
Na aula null e Tipos Anuláveis, cada null pedia um if: "tem valor? então usa; não tem? então faz outra coisa". Isso aparece tanto em C# que a linguagem ganhou três operadores só para isso. O ?? dá um valor reserva, o ?. só continua se houver algo e o ??= preenche o que está em branco. Com eles, um if de quatro linhas vira meia linha.
- O
??("ou então"):apelido ?? nome. - O
?.e o?[]("se existir"):nome?.Length,notas?[0], e por que o resultado vira anulável. - A dupla mais usada do C#:
nome?.Length ?? 0. - O
??=("se estiver em branco, preencha"):cidade ??= "Recife". - A atribuição condicional nula do C# 14:
estoque?[0] = 10. - A armadilha da precedência:
50 + bonus ?? 0dá 0! - Os erros reais:
CS0019,CS0023,CS0266,CS9260e aNullReferenceException.
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: o plano B
A Ana vai buscar a Bia na escola. Ela combina: "se a sua mãe não puder, então eu vou". Isso é o ??: use o primeiro, ou então o reserva.
Na portaria, o porteiro só chama a criança se ela estiver lá; se não estiver, ele não sai procurando pelo prédio, só responde "não está". Isso é o ?.: siga em frente só se existir; se não existir, a resposta é null, sem quebrar nada.
E na lista de presença, a professora escreve "faltou" só nos campos em branco; quem já tem algo escrito fica como está. Isso é o ??=.
??: o valor reserva
O operador ?? (o nome oficial é "operador de coalescência nula") olha o valor da esquerda. Se ele não for null, o resultado é ele. Se for null, o resultado é o valor da direita:
int? bonus = null;
int pontos = 50 + (bonus ?? 0);
Console.WriteLine($"Pontos: {pontos}");
string? apelido = null;
string nome = "Ana";
Console.WriteLine($"Olá, {apelido ?? nome}!");
int? a = null;
int? b = null;
int c = 7;
Console.WriteLine(a ?? b ?? c);
$ dotnet run coalesc.cs Pontos: 50 « bonus é null: valeu o 0 Olá, Ana! « apelido é null: valeu o nome 7 « a e b são null: valeu o c
- bonus ?? 0
- Leia "
bonus, ou então 0". Comobonusénull, vale 0. Se valesse 5, valeria 5. Repare: o resultado é umintcomum, sem?, porque o reserva (0) nunca énull. Por isso a soma cabe emint pontos. - (bonus ?? 0)
- Os parênteses são importantes aqui. Sem eles, a conta sai errada; veja a armadilha mais abaixo.
- apelido ?? nome
- Funciona com textos também: se a Ana tiver apelido, usa o apelido; se não, o nome.
- a ?? b ?? c
- Uma corrente de reservas: o primeiro que não for
nullganha. Lê-se da esquerda para a direita: "a, ou entãob, ou entãoc".
O ?? é o atalho de duas coisas que você já conhece. As três linhas abaixo dão o mesmo resultado:
int x1 = bonus.HasValue ? bonus.Value : 0; // com o ternário int x2 = bonus.GetValueOrDefault(0); // com o método int x3 = bonus ?? 0; // com o ??
A primeira usa o operador ternário, a segunda é da aula anterior. A terceira é a que você vai ver em quase todo código C#, porque também funciona com string (que não tem GetValueOrDefault).
Um uso de todo dia: Console.ReadLine()
O Console.ReadLine() devolve string?: quando não há mais nada para ler (por exemplo, quando a entrada vem de um arquivo vazio), ele devolve null. Com o ??, o programa sempre tem um texto:
Console.Write("Seu nome: ");
string nome = Console.ReadLine() ?? "visitante";
Console.WriteLine($"Olá, {nome}!");
$ echo Ana | dotnet run leitura.cs Seu nome: Olá, Ana! $ dotnet run leitura.cs < /dev/null « /dev/null: uma entrada vazia Seu nome: Olá, visitante!
Sem o ?? "visitante", o compilador avisa que ReadLine() pode devolver null e você está guardando isso numa string que não aceita nulo:
$ dotnet run leitura0.cs < /dev/null /home/ana/cs/leitura0.cs(2,15): warning CS8600: Conversão de literal nula ou possível valor nulo em tipo não anulável. Seu nome: Olá, !
Esse aviso é explicado com calma em Referências Anuláveis.
Missão: rode e veja os avisos (CS8600: um possível null numa string; CS0219: o nome nunca é usado) e a chamada em branco. A Bia não tem apelido, então a chamada deve usar o nome como reserva. Conserte a linha 3 com ??, para mostrar Chamada: Bia sem nenhum aviso.
string? apelido = null;
string nome = "Bia";
string chamada = apelido;
Console.WriteLine($"Chamada: {chamada}");
string? apelido = null;
string nome = "Bia";
string chamada = apelido ?? nome;
Console.WriteLine($"Chamada: {chamada}");
?.: só continua se existir
Na aula anterior, nome.Length com nome valendo null derrubou o programa com NullReferenceException. O ?. (o "operador condicional nulo") resolve: se a esquerda for null, ele não tenta o que vem depois e o resultado inteiro é null. Se não for, funciona como o ponto comum:
string? nome = null;
Console.WriteLine($"[{nome?.ToUpper()}]");
Console.WriteLine($"[{nome?.Length}]");
Console.WriteLine(nome?.Length ?? 0);
nome = "ana";
Console.WriteLine($"[{nome?.ToUpper()}]");
Console.WriteLine($"[{nome?.Length}]");
$ dotnet run cond.cs [] « nome é null: nada de ToUpper, o resultado é null [] « nem de Length 0 « null ?? 0 = 0 [ANA] « agora nome existe: ?. funciona como o ponto [3]
- nome?.ToUpper()
- Leia "se
nomeexistir,ToUpper()". Comnull, o método nem é chamado. - nome?.Length
- O tamanho do texto, se existir. Atenção ao tipo:
Lengthé umint, masnome?.Lengthé umint?, porque pode darnull. Guardar isso numinté o erroCS0266. - nome?.Length ?? 0
- A dupla mais famosa: "o tamanho do nome, se existir; senão, 0". O
?.evita a quebra, e o??troca onullpor um número. O resultado é umintcomum.
?[]: o mesmo para índices
Um array também pode ser null (um int[]? é "um array, ou nenhum array"). Para pegar uma posição só se o array existir, use ?[]:
int[]? notas = null;
Console.WriteLine($"[{notas?[0]}]"); // []
Console.WriteLine(notas?.Length ?? 0); // 0
notas = new int[] { 8, 9 };
Console.WriteLine($"[{notas?[0]}]"); // [8]
O ?[] protege só contra o array ser null. Ele não protege contra um índice que não existe: notas?[5] num array de 2 posições continua dando IndexOutOfRangeException (veja Arrays).
Uma corrente para no primeiro null
Quando você escreve nome?.Trim().ToUpper(), o ?. protege a corrente inteira: se nome for null, nem o Trim() nem o ToUpper() rodam. Não precisa de ?. em cada ponto.
string? nome = " bia ";
Console.WriteLine($"[{nome?.Trim().ToUpper()}]");
nome = null;
Console.WriteLine($"[{nome?.Trim().ToUpper()}]");
Console.WriteLine(nome?.Trim().Length ?? 0);
$ dotnet run cadeia.cs [BIA] [] « parou no nome: Trim e ToUpper nem rodaram 0
Missão: rode e veja o programa quebrar no segundo nome, que é null (código de saída 134). Use ?. e ?? na linha do WriteLine para que o nome em branco apareça como (sem nome) tem 0 letras e o programa vá até o fim.
string?[] nomes = { "ana", null, "caio" };
foreach (string? n in nomes)
Console.WriteLine($"{n.ToUpper()} tem {n.Length} letras");
string?[] nomes = { "ana", null, "caio" };
foreach (string? n in nomes)
Console.WriteLine($"{n?.ToUpper() ?? "(sem nome)"} tem {n?.Length ?? 0} letras");
??=: preencha se estiver em branco
O ??= é para variáveis: "se esta variável for null, ponha este valor nela; se já tiver algo, deixe como está".
string? cidade = null; cidade ??= "Recife"; Console.WriteLine(cidade); cidade ??= "Natal"; Console.WriteLine(cidade); int? estoque = null; estoque ??= 10; Console.WriteLine(estoque);
$ dotnet run igual.cs Recife « estava em branco: recebeu "Recife" Recife « já tinha valor: o "Natal" foi ignorado 10
- cidade ??= "Recife";
- É o atalho de
cidade = cidade ?? "Recife";, ou deif (cidade is null) cidade = "Recife";. Assim como o+=dos operadores de atribuição, ele lê e grava a mesma variável. - cidade ??= "Natal";
- Não muda nada: a cidade já era "Recife". O
??=nunca apaga um valor que já existe.
O uso típico é dar um valor padrão a algo que talvez já tenha sido preenchido antes, por exemplo uma configuração que o usuário pode ou não ter informado.
Atribuição condicional nula C# 14
Até o C# 13, o ?. e o ?[] só serviam para ler. No C# 14, eles também podem ficar do lado esquerdo de um =: "se existir, grave; se não existir, não faça nada".
int[]? estoque = null;
estoque?[0] = 10;
Console.WriteLine("Nada quebrou.");
estoque = new int[3];
estoque?[0] = 10;
Console.WriteLine($"[{estoque?[0]}]");
$ dotnet run estoque.cs Nada quebrou. « estoque era null: a gravação foi pulada [10] « agora o array existe: gravou
Sem o ?, a primeira gravação derruba o programa (e o compilador avisa antes, com o CS8602):
$ dotnet run semq.cs; echo "código: $?" /home/ana/cs/semq.cs(2,1): warning CS8602: Desreferência de uma referência possivelmente nula. Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in /home/ana/cs/semq.cs:line 2 código: 134
Em versões antigas da linguagem, a atribuição condicional não existe. Dá para ver isso pedindo ao compilador o C# 13 com a linha #:property LangVersion=13 no topo do arquivo (as linhas #: são configurações do dotnet run arquivo.cs):
$ dotnet run atrib13.cs /home/ana/cs/atrib13.cs(3,11): error CS9260: O recurso "atribuição condicional nula" não está disponível no C# 13.0. Use a versão da linguagem 14.0 ou superior. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O uso mais comum dessa novidade é com objetos, como cliente?.Nome = "Ana";. Você vai criar seus próprios objetos a partir de Classes e Objetos.
A armadilha da precedência
O ?? tem baixa precedência: o C# faz as contas (+, -, *…) antes dele. Veja o que acontece sem parênteses:
int? bonus = null;
int pontos = 50 + bonus ?? 0;
Console.WriteLine($"Pontos: {pontos}");
int certo = 50 + (bonus ?? 0);
Console.WriteLine($"Certo: {certo}");
$ dotnet run prec.cs Pontos: 0 « os 50 pontos sumiram! Certo: 50
Compilou sem erro e sem aviso, e o resultado está errado. O C# leu a linha 2 como (50 + bonus) ?? 0: primeiro 50 + null, que dá null (conta com null dá null, como você viu na aula anterior), e depois null ?? 0, que dá 0. Regra prática: sempre ponha o ?? entre parênteses quando ele estiver no meio de uma conta.
Formas diferentes e qual usar
| Você quer… | Com if | Com operador |
|---|---|---|
| um valor reserva | if (x is null) r = 0; else r = x.Value; | r = x ?? 0; |
| usar algo só se existir | if (nome is not null) t = nome.Length; | t = nome?.Length; (um int?) |
| as duas coisas | t = nome is null ? 0 : nome.Length; | t = nome?.Length ?? 0; |
| preencher se estiver em branco | if (c is null) c = "Recife"; | c ??= "Recife"; |
| gravar só se existir C# 14 | if (v is not null) v[0] = 10; | v?[0] = 10; |
Use os operadores quando a regra cabe numa frase ("o nome, ou então visitante"). Use o if quando, além de trocar o valor, você precisa fazer algo diferente, como mostrar uma mensagem ou pedir o dado de novo.
Quando dá errado
?? num tipo que nunca é null
$ dotnet run e1.cs /home/ana/cs/e1.cs(2,9): error CS0019: O operador "??" não pode ser aplicado a operandos dos tipos "int" e "int" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: int idade = 29; int x = idade ?? 0;. Um int comum nunca é null, então não faz sentido ter reserva. O idade ??= 0; dá o mesmo erro, com "??=" na mensagem.
Como resolver: se a variável pode ficar em branco, declare-a como int?. Se não pode, apague o ?? 0.
$ dotnet run e5.cs /home/ana/cs/e5.cs(2,29): error CS0019: O operador "??" não pode ser aplicado a operandos dos tipos "int?" e "string" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: idade ?? "não informada", com idade do tipo int?. Os dois lados do ?? precisam combinar: o resultado é ou a idade ou o texto, e o C# não sabe se isso é número ou texto.
Como resolver: transforme a idade em texto primeiro, com ?.ToString(): idade?.ToString() ?? "não informada". Agora os dois lados são texto.
?. é anulável
$ dotnet run e2.cs /home/ana/cs/e2.cs(2,15): error CS0266: Não é possível converter implicitamente tipo "int?" em "int". Existe uma conversão explícita (há uma conversão ausente?) /home/ana/cs/e2.cs(2,15): warning CS8629: O tipo de valor de nulidade pode ser nulo. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: int tamanho = nome?.Length;. Como nome pode ser null, nome?.Length pode ser null, e é um int?.
Como resolver: dê um reserva: int tamanho = nome?.Length ?? 0;.
?. num tipo de valor
$ dotnet run e6.cs /home/ana/cs/e6.cs(2,24): error CS0023: O operador "?" não pode ser aplicado ao operando do tipo "int" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: int idade = 29; Console.WriteLine(idade?.ToString());. De novo: um int nunca é null, então não há o que conferir.
Como resolver: use o ponto comum, idade.ToString(). O ?. só serve para o que pode ser null: int?, string?, arrays e objetos.
?. ficou pela metade: NullReferenceException
string? nome = null; string grito = nome?.ToUpper(); Console.WriteLine(grito.Length);
$ dotnet run e3.cs; echo "código: $?" /home/ana/cs/e3.cs(2,16): warning CS8600: Conversão de literal nula ou possível valor nulo em tipo não anulável. /home/ana/cs/e3.cs(3,19): warning CS8602: Desreferência de uma referência possivelmente nula. Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in /home/ana/cs/e3.cs:line 3 código: 134
O que aconteceu: o ?. da linha 2 evitou a quebra ali, mas o resultado (null) foi guardado em grito. Na linha 3, o ponto comum em grito.Length quebrou. O ?. não faz o null desaparecer: ele só o passa adiante. Os dois avisos apontaram as duas linhas.
Como resolver: troque o null por um valor na hora de guardar, com ??: string grito = nome?.ToUpper() ?? "";. Ou continue usando ?. e ?? até o fim: grito?.Length ?? 0.
$ dotnet run prec.cs Pontos: 0 Certo: 50
O que aconteceu: 50 + bonus ?? 0 é lido como (50 + bonus) ?? 0.
Como resolver: 50 + (bonus ?? 0).
Referência
| Escrita | Lê-se | Resultado |
|---|---|---|
a ?? b | "a, ou então b" | a se não for null; senão, b |
a ?? b ?? c | "o primeiro que existir" | o primeiro que não for null |
x?.Membro | "se x existir, Membro" | null se x for null; o resultado é anulável (int?…) |
x?[i] | "se x existir, a posição i" | null se o array for null |
x?.Membro ?? padrão | "o membro, ou então o padrão" | nunca null |
x ??= v; | "se x estiver em branco, ponha v" | o mesmo que x = x ?? v; |
x?[i] = v;, x?.Membro = v; | "se x existir, grave" C# 14 | nada acontece se x for null |
Missão final: o cadastro incompleto
Missão: o cadastro da Ana chegou todo em branco, e o programa quebra na última linha. Conserte usando os três operadores da aula:
- com
??=, dê aoapelidoo valor"Aninha"se ele estiver em branco; - mostre a idade, ou
não informada(dica:idade?.ToString() ?? "não informada"); - mostre a quantidade de notas, ou 0 se não houver array (
?.e??).
string? apelido = null;
int? idade = null;
int[]? notas = null;
Console.WriteLine($"Apelido: {apelido}");
Console.WriteLine($"Idade: {idade}");
Console.WriteLine($"Notas: {notas.Length}");
string? apelido = null;
int? idade = null;
int[]? notas = null;
apelido ??= "Aninha";
Console.WriteLine($"Apelido: {apelido}");
Console.WriteLine($"Idade: {idade?.ToString() ?? "não informada"}");
Console.WriteLine($"Notas: {notas?.Length ?? 0}");
Exercícios
1. Valor reserva
Complete para que cor seja favorita ou, se ela estiver em branco, "azul":
2. Tamanho seguro
Complete para pegar o tamanho de nome sem quebrar quando ele for null (o resultado pode ser int?):
3. Preencher o branco
Complete para que cidade receba "Recife" só se estiver em branco:
4. Com int? bonus = null;, quanto vale 10 + bonus ?? 5?
5. Qual é o tipo de nome?.Length, sendo nome um string??
6. Depois de string? c = "Natal"; c ??= "Recife";, quanto vale c?
Resumo
a ?? b:a, ou entãob. Encadeia:a ?? b ?? c.x?.Membroex?[i]: só continuam sexexistir; senão, dãonull. O resultado é anulável (nome?.Lengthéint?). A corrente para no primeironull.nome?.Length ?? 0: a dupla que nunca quebra e nunca dánull.x ??= v: preenche só se estiver em branco; nunca apaga um valor que já existe.- C# 14
x?[i] = vex?.Membro = v: gravam só sexexistir (antes,CS9260). - O
??tem precedência baixa:50 + (bonus ?? 0), sempre com parênteses. - Esses operadores só servem para o que pode ser
null; numintcomum dãoCS0019ouCS0023. - Próxima aula: Referências Anuláveis, o sistema que gera os avisos
CS8600eCS8602.