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.

Você vai aprender
  • 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 ?? 0 dá 0!
  • Os erros reais: CS0019, CS0023, CS0266, CS9260 e a NullReferenceException.

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". Como bonus é null, vale 0. Se valesse 5, valeria 5. Repare: o resultado é um int comum, sem ?, porque o reserva (0) nunca é null. Por isso a soma cabe em int 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 null ganha. Lê-se da esquerda para a direita: "a, ou então b, ou então c".

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 nome existir, ToUpper()". Com null, o método nem é chamado.
nome?.Length
O tamanho do texto, se existir. Atenção ao tipo: Length é um int, mas nome?.Length é um int?, porque pode dar null. Guardar isso num int é o erro CS0266.
nome?.Length ?? 0
A dupla mais famosa: "o tamanho do nome, se existir; senão, 0". O ?. evita a quebra, e o ?? troca o null por um número. O resultado é um int comum.

?[]: 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 de if (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 ifCom operador
um valor reservaif (x is null) r = 0; else r = x.Value;r = x ?? 0;
usar algo só se existirif (nome is not null) t = nome.Length;t = nome?.Length; (um int?)
as duas coisast = nome is null ? 0 : nome.Length;t = nome?.Length ?? 0;
preencher se estiver em brancoif (c is null) c = "Recife";c ??= "Recife";
gravar só se existir C# 14if (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

CS0019: ?? 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.

CS0019: reserva de outro tipo
$ 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.

CS0266: o resultado do ?. é 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;.

CS0023: ?. 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.

O ?. 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.

Sem erro, mas com o resultado errado: a precedência
$ 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

EscritaLê-seResultado
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# 14nada 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:

  1. com ??=, dê ao apelido o valor "Aninha" se ele estiver em branco;
  2. mostre a idade, ou não informada (dica: idade?.ToString() ?? "não informada");
  3. 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":

string cor = favorita "azul";

2. Tamanho seguro

Complete para pegar o tamanho de nome sem quebrar quando ele for null (o resultado pode ser int?):

int? tamanho = nome;

3. Preencher o branco

Complete para que cidade receba "Recife" só se estiver em branco:

cidade "Recife";

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ão b. Encadeia: a ?? b ?? c.
  • x?.Membro e x?[i]: só continuam se x existir; senão, dão null. O resultado é anulável (nome?.Length é int?). A corrente para no primeiro null.
  • 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] = v e x?.Membro = v: gravam só se x existir (antes, CS9260).
  • O ?? tem precedência baixa: 50 + (bonus ?? 0), sempre com parênteses.
  • Esses operadores só servem para o que pode ser null; num int comum dão CS0019 ou CS0023.
  • Próxima aula: Referências Anuláveis, o sistema que gera os avisos CS8600 e CS8602.