Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Gorjeta
Novo no desenvolvimento de software? Comece com os tutoriais de Introdução .
Experimentou em outro idioma? Se você trabalhou com tipos anuláveis do Kotlin, TypeScript strictNullChecksou opcionais de Swift, o modelo será familiar. O C# usa análise estática e diagnóstico de aviso em vez de um tipo separado. Dê uma olhada rápida em Expressar intenção com anotações e Análise de estado de nulidade e, em seguida, vá para o Tutorial: Expresse sua intenção de projeto com tipos de referência anuláveis e não anuláveis para usar o recurso.
Tipos de referência anuláveis são um conjunto de recursos que minimizam a chance de que seu código lance System.NullReferenceException. Você declara quais variáveis se destinam a conter null e quais não, e o compilador avisa quando essas declarações não correspondem à forma como seu código as usa. O comportamento de runtime do seu programa permanece inalterado. Tipos de referência anuláveis são um recurso exclusivo do tempo de compilação.
Três blocos de construção funcionam juntos:
-
Anotações variáveis (
stringvs.string?) expressam quais referências se destinam a permitirnull. - A análise de estado nulo controla se o valor de uma expressão não é nulo ou talvez nulo em cada ponto do código.
-
Atributos em APIs descrevem contratos com mais nuances, como "esse argumento pode ser
null, mas o valor retornado é nulo somente quando o argumento é nulo".
O compilador combina esses sinais para produzir diagnósticos. Avisos sobre uma variável não anulável indicam que a variável pode receber o valor null. Avisos em uma variável anulável significam que o código pode desreferi-lo sem uma verificação nula.
Desreferência significa usar o valor ao qual a variável se refere. Por exemplo, para chamar um método nele (variable.Method()), leia uma propriedade (variable.Property) ou indexe-a (variable[0]). Desreferenciar uma variável que tem um valor de null gera uma exceção em tempo de execução. Qualquer tipo de aviso significa que o comportamento do código não corresponde ao design declarado.
Contexto que permite valor nulo
Projetos criados com base em modelos de .NET recentes definem <Nullable>enable</Nullable> no arquivo de projeto, portanto, as diretrizes neste artigo se aplicam conforme escrito. Se você estiver trabalhando em um projeto mais antigo, abra o .csproj e verifique se o <PropertyGroup> contém a seguinte linha; adicione-a se estiver faltando:
<Nullable>enable</Nullable>
Para obter mais informações sobre como migrar um aplicativo grande, consulte o artigo sobre estratégias de migração anuláveis para obter mais configurações e diretivas.
Expresse a intenção com anotações
Cada variável de tipo de referência é não anulável por padrão. Acrescente ? para declarar um tipo de referência anulável :
public static void Annotations()
{
string required = "always set"; // non-nullable: assigning null produces a warning
string? optional = null; // nullable: holding null is allowed
Console.WriteLine(required.Length);
if (optional is not null)
{
Console.WriteLine(optional.Length);
}
}
A anotação não altera o tipo de runtime.
string e string? são ambos System.String. O ? informa o compilador da sua intenção de design. Essa intenção formata os avisos que o compilador produz:
- Uma variável não anulável tem como estado de nulidade padrão não nulo. O compilador avisará se você atribuir um valor que pode ser
null. - Uma variável anulável tem um estado nulo padrão de maybe-null. O compilador avisará se você desreferenciar a variável sem primeiro verificá-la.
Use a anotação para tornar os valores obrigatórios e opcionais visíveis no sistema de tipos. O tipo a seguir Person declara FirstName e LastName como não anulável — cada pessoa deve ter ambos — e MiddleName como anulável, porque nem todos têm um:
public sealed class Person(string firstName, string lastName)
{
public string FirstName { get; } = firstName;
public string? MiddleName { get; init; }
public string LastName { get; } = lastName;
public override string ToString() => MiddleName is null
? $"{FirstName} {LastName}"
: $"{FirstName} {MiddleName} {LastName}";
}
public static void DesignIntent()
{
Person p1 = new("Ada", "Lovelace") { MiddleName = "King" };
Console.WriteLine(p1);
// Output: Ada King Lovelace
Person p2 = new("Grace", "Hopper");
Console.WriteLine(p2);
// Output: Grace Hopper
}
As anotações impulsionam a implementação de ToString. Como FirstName e LastName não admitem valor nulo, a sobrescrita usa ambos diretamente em uma cadeia de caracteres interpolada (a sintaxe $"...", que insere expressões em marcadores {}) sem verificação de nulo.
MiddleName pode ser nulo, portanto, a sobrescrita primeiro a verifica em relação a null e só a inclui quando ela está presente. O compilador impõe essa distinção: o código que passa um valor possivelmente nulo onde se espera um valor não anulável produz um aviso, e um construtor que deixa um membro não anulável sem inicialização também produz um aviso.
Análise de estado nulo
O compilador rastreia o estado nulo de cada expressão. O estado é um dos dois valores:
-
not-null: sabe-se que a expressão não é
null. -
maybe-null: a expressão pode ser
null.
O estado nulo de uma variável local é atualizado à medida que o compilador analisa seu código. Duas coisas mudam: atribuições e verificações nulas. Após uma atribuição, o estado nulo da variável corresponde à expressão no lado direito. Se a expressão for nula ou anulável, a variável se tornará talvez nula. Se a expressão for um literal não nulo, a variável se tornará não nula. Após uma verificação nula, o estado nulo da variável reflete qualquer ramificação tomada.
public static void NullStateTracking()
{
string? message = null;
// Warning: dereference of a possibly null reference.
Console.WriteLine(message.Length);
message = "Hello, World!";
// No warning: the compiler tracks that message is now not-null.
Console.WriteLine(message.Length);
}
No exemplo anterior, a primeira desreferência produz um aviso porque messagetalvez seja nulo. Após a atribuição a um literal não nulo, o compilador sabe message que não é nulo, portanto, a segunda dereferência é segura.
A análise de estado nulo funciona entre if verificações, correspondência de padrões (expressões como is null ou is { } que testam a forma de um valor) e o fluxo de controle que faz loops ou retorna antecipadamente:
public sealed class Node(string name)
{
public string Name { get; } = name;
public Node? Parent { get; init; }
}
public static void FlowAnalysis(Node start)
{
Node? current = start;
while (current is not null)
{
// Inside the loop, the compiler knows current is not-null.
Console.WriteLine(current.Name);
current = current.Parent;
}
}
A análise não examina o corpo dos métodos. Se você precisar de um método para comunicar o estado nulo aos chamadores, use atributos de análise anuláveis em sua assinatura.
Ignorar os avisos com !
Às vezes, você sabe mais do que o compilador. O operador ! declara que uma expressão não é nula, mesmo quando a análise diz o contrário:
public static void NullForgiving()
{
// "ada" matches a switch arm that returns a non-null string,
// but the return type is string? so the compiler treats the
// result as maybe-null.
string? maybeName = LookUpName("ada");
// The ! tells the compiler "trust me, this isn't null." We just
// passed "ada", which the switch maps to "Ada Lovelace".
int length = maybeName!.Length;
Console.WriteLine(length); // => 12
}
// Returns string? because the wildcard arm yields null.
private static string? LookUpName(string id) => id switch
{
"ada" => "Ada Lovelace",
_ => null,
};
Use ! com moderação. Cada ocorrência é um local em que o compilador não pode mais protegê-lo. Prefira adicionar uma verificação nula, reestruturar o código ou anotar a API relevante para que o compilador chegue à conclusão certa por conta própria.
Atributos que descrevem contratos de API
Anotações em um parâmetro ou tipo de retorno nem sempre são expressivas o suficiente. Um método pode aceitar um argumento possivelmente nulo, mas garantir um resultado não nulo. Um método de teste pode retornar true somente quando seu argumento não for nulo. Use os atributos de análise anuláveis para transmitir esses contratos:
public static bool IsPresent([NotNullWhen(true)] string? value) =>
!string.IsNullOrEmpty(value);
public static void NullAnalysisAttributes()
{
string? input = ReadInput();
if (IsPresent(input))
{
// No null-forgiving operator needed: the attribute tells the compiler
// input is not-null when IsPresent returns true.
Console.WriteLine(input.Length);
}
}
private static string? ReadInput() => "hello";
O NotNullWhenAttribute informa ao compilador que, quando IsPresent retorna true, o argumento não é nulo. Dentro do bloco if, o compilador trata value como não nula, sem exigir o operador de supressão de nulidade. A partir de .NET 5, todas as APIs de runtime .NET são anotadas, portanto, a análise beneficia qualquer código que as chame.
Armadilhas conhecidas
Dois padrões podem fazer com que uma referência não anulável contenha null sem aviso. Ambos os padrões são limitações da análise estática, não bugs em seu código.
Estruturas padrão
Você pode criar um struct com campos de referência não anuláveis usando default ou new(). Essa abordagem deixa os campos do struct não inicializados:
public struct Student
{
public string FirstName;
public string? MiddleName;
public string LastName;
}
public static void DefaultStructPitfall()
{
Student s = default; // No warning, but FirstName and LastName are null.
Console.WriteLine(s.FirstName?.Length ?? -1);
}
Os campos contêm null em tempo de execução, mas o compilador não emite um aviso. Se você precisar usar um struct, prefira os membros necessários, que são membros que o chamador deve inicializar por meio de um inicializador de objeto ou um construtor parametrizado que os chamadores devem invocar.
Arrays de referências e estruturas
Uma nova matriz de um tipo de referência não anulável contém todos os null elementos até que você atribua cada um:
public static void ArrayPitfall()
{
string[] values = new string[3]; // Elements are null at run time.
Console.WriteLine(values[0]?.Length ?? -1);
string[] initialized = ["a", "b", "c"]; // Collection expression initializes every slot.
Console.WriteLine(initialized[0].Length);
}
A mesma armadilha se aplica a matrizes de structs: cada elemento começa como o valor padrão do struct, portanto, os campos de referência não anuláveis de cada elemento começam como null.
Inicialize elementos de matriz como parte da criação da matriz.
Expressões de coleção (a sintaxe literal [1, 2, 3]) e com tipo de destino new (escrever new() quando o compilador pode inferir o tipo) tornam a inicialização completa mais concisa.