null significa "nenhum valor". Tipos de referência (classes, strings, arrays) sempre puderam guardá-lo. Tipos de valor (int, decimal, bool, DateTime, structs) não podem, a menos que você os torne anuláveis com ?. C# então oferece um pequeno conjunto de operadores para trabalhar com valores que podem estar ausentes: ??, ??= e ?..
Tipos de valor anuláveis: int?
Acrescente ? a um tipo de valor para permitir null. int? é a abreviação da struct Nullable<int>, que guarda um valor mais uma flag dizendo se o valor está presente:
Saída:
False
True
True
5.50
0
3
Rated 4 stars
4
As fontes típicas de tipos de valor anuláveis são campos opcionais de formulário, parâmetros de consulta e colunas NULL em um banco de dados: um Order com um DateTime? ShippedAt ainda não foi enviado enquanto o valor for null. A alternativa, um valor mágico como -1 ou DateTime.MinValue, parece um dado real e mais cedo ou mais tarde é usado como um.
Um int? só se converte para int de forma explícita, porque a conversão pode falhar. Isso leva à única exceção que os tipos de valor anuláveis lançam.
"Nullable object must have a value"
Ler o .Value quando não há valor, ou fazer o cast de um int? null para int, lança InvalidOperationException:
Saída:
InvalidOperationException: Nullable object must have a value.
0
0
not stocked
stock is int count é um padrão: ele testa se há um valor e o desembrulha em count em um passo, o que se lê melhor que HasValue mais Value. A página de pattern matching tem mais formas.
Aritmética e comparações com null
Os operadores em tipos de valor anuláveis são elevados (lifted): se algum dos operandos for null, o resultado é null. As comparações de ordem (<, >, <=, >=) são a exceção: elas retornam false em vez de null. (A outra exceção é bool? com & e |, em que null & false é false.) Por exemplo:
Saída:
True
[]
False
False
True
True
Tanto a > 5 quanto a <= 5 são falsos, então !(a > 5) não é o mesmo que a <= 5 quando a é null. Um código como if (!(age >= 18)) Deny(); trata uma idade ausente como menor de idade, o que pode ou não ser o que você queria. Decida primeiro o que null deve significar, normalmente com ??, e depois compare.
O operador de coalescência nula ??
a ?? b resulta em a quando a não é null, e em b caso contrário. Ele funciona em tipos de valor anuláveis e em tipos de referência, e pode ser encadeado da esquerda para a direita:
Saída:
dark
16px
20
ANONYMOUS
O lado direito só é avaliado quando necessário, então cache ?? LoadFromDatabase() não vai ao banco de dados quando o cache tem um valor. ?? também aceita uma expressão throw, o que dá uma guarda compacta (veja throw):
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
??= (C# 8)
x ??= y atribui y a x só se x for null. É o jeito idiomático de fazer inicialização preguiçosa:
private List<string> tags;
public void AddTag(string tag)
{
tags ??= new List<string>(); // C# 8 and later
tags.Add(tag);
}
Antes do C# 8, escreva tags = tags ?? new List<string>(); ou if (tags == null) tags = new List<string>();.
O operador condicional nulo ?.
a?.Member só avalia Member se a não for null; caso contrário, a expressão inteira é null. Ele transforma uma cadeia de verificações de null em uma única expressão:
Saída:
Braga
(no city)
(no customer)
0
True
handler: saved
Detalhes que vale conhecer:
- O operador interrompe o resto da cadeia: em
nobody?.Address?.City, quandonobodyé null nada à direita executa. - Quando o membro retorna um tipo de valor, o resultado vira anulável:
Orders?.Countéint?, e é por isso que precisa de?? 0para voltar a ser umint. ?.não pode ser o alvo de uma atribuição antes do C# 14:customer?.Name = "x"não compila nas versões anteriores.handler?.Invoke(...)é a forma padrão de disparar um evento ou chamar um callback opcional.
Use ?. onde null é um estado legítimo. Espalhá-lo por toda parte para evitar exceções esconde o bug que produziu o null inesperado, e o programa continua com dados faltando em vez de falhar onde está o problema.
Verificando null
if (customer == null) { ... } // classic
if (customer is null) { ... } // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... } // C# 9
if (customer is { } c) { ... } // C# 8 property pattern: not null, and named
is null sempre faz uma verificação real de null, enquanto == chama a sobrecarga do operador do tipo, se houver uma, então uma sobrecarga mal escrita pode fazer x == null mentir. Para argumentos que não podem ser null, falhe cedo:
public void Ship(Order order)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6+
// ...
}
Tipos de referência anuláveis (C# 8)
Acrescentar ? a um tipo de valor muda o tipo. Acrescentar ? a um tipo de referência, desde o C# 8, muda só o que o compilador verifica. Com o recurso ativado (<Nullable>enable</Nullable> no arquivo de projeto, o padrão para projetos novos desde o .NET 6), string significa "nunca deveria ser null" e string? significa "pode ser null", e o compilador avisa quando o código não corresponde:
#nullable enable
public class User
{
public string Email { get; set; } // warning CS8618: Non-nullable property 'Email'
// must contain a non-null value when exiting constructor.
public string? Nickname { get; set; } // allowed to be null
public int NicknameLength() => Nickname.Length;
// warning CS8602: Dereference of a possibly null reference.
public int SafeLength() => Nickname?.Length ?? 0; // no warning
}
string? input = Console.ReadLine();
string name = input; // warning CS8600: Converting null literal or possible
// null value to non-nullable type.
string checkedName = input ?? "guest"; // no warning
int len = input!.Length; // ! tells the compiler "trust me": no warning
O compilador acompanha as suas verificações pelo método: depois de if (input != null), input conta como não null. O operador ! (null-forgiving) silencia um aviso quando você sabe mais que a análise; cada um é uma afirmação que o compilador não consegue verificar, então use-o raramente.
Dois fatos esclarecem a maior parte da confusão. Primeiro, isso só gera avisos: o programa executa igual, e uma string ainda pode guardar null em tempo de execução se veio de código sem anotações. Segundo, string? não é Nullable<string>; não existe HasValue nela.
Erros comuns
.Valuesem verificação. LançaInvalidOperationExceptionquando é null. Use??,GetValueOrDefault()ouis int n.- Supor que
!(x > 5)significax <= 5. Não quandoxé null: toda comparação de ordem com null é falsa. - Cobrir todo null com
?.. Isso silencia o sintoma e afasta a falha da causa. - Tratar os avisos de referência anulável como ruído. Cada um marca uma linha que pode lançar
NullReferenceException. Corrija o fluxo ou declare a intenção com?. - Valores mágicos no lugar de null.
-1,0eDateTime.MinValuepara "desconhecido" parecem dados reais. Useint?eDateTime?.
Perguntas frequentes
O que int? significa em C#?
int? é a abreviação de Nullable<int>: um int que também pode ser null. Tipos de valor como int, decimal, bool e DateTime normalmente sempre têm um valor; acrescentar ? dá a eles um estado de "nenhum valor", que é o que um campo de formulário vazio ou uma coluna NULL do banco de dados precisam. Verifique com HasValue ou != null e leia com Value ou GetValueOrDefault().
O que o ?? faz em C#?
a ?? b é o operador de coalescência nula: resulta em a se a não for null, senão em b. string name = input ?? "guest"; fornece um valor alternativo em uma expressão. Ele pode ser encadeado (a ?? b ?? c pega o primeiro valor não nulo), e b só é avaliado quando necessário. ??= (C# 8) atribui o lado direito só quando a variável é null.
O que o ?. faz em C#?
a?.B é o operador condicional nulo: se a for null, a expressão inteira é null e B nunca é avaliado, então não há NullReferenceException. Ele pode ser encadeado (order?.Customer?.Address?.City) e funciona em chamadas de métodos (logger?.Log(...)), eventos (Changed?.Invoke(...)) e indexadores (items?[0]). Combine-o com ?? para um padrão: order?.Customer?.Name ?? "unknown".
O que causa "Nullable object must have a value"?
Ler o .Value de um tipo de valor anulável que é null, ou fazer o cast dele para o tipo não anulável ((int)maybe), lança InvalidOperationException com essa mensagem. Verifique HasValue antes, ou use GetValueOrDefault(), ?? fallback ou um padrão: if (maybe is int n) { ... }.
Como verificar se algo é null em C#?
x == null e x is null funcionam; is null (C# 7) não pode ser afetado por um tipo que sobrecarrega ==, e por isso muitos códigos o preferem. A negação é x != null, ou x is not null no C# 9. Para rejeitar argumentos null, use ArgumentNullException.ThrowIfNull(x) (.NET 6) ou _ = x ?? throw new ArgumentNullException(nameof(x)); em código mais antigo.
O que são tipos de referência anuláveis em C#?
Um recurso do compilador do C# 8, ativado com <Nullable>enable</Nullable> (o padrão nos projetos novos do .NET 6 em diante). Com ele ativo, string significa "nunca null" e string? significa "pode ser null", e o compilador avisa quando você pode acessar um null (CS8602) ou atribuir null a uma variável não anulável (CS8600). Ele só muda os avisos, não o comportamento em tempo de execução.