Noções básicas sobre nulidade
Se você for desenvolvedor do .NET, é provável que tenha encontrado o System.NullReferenceException. Isso ocorre em tempo de execução quando um null
é desreferenciado, ou seja, quando uma variável é avaliada em runtime, mas a variável se refere a null
. Essa exceção é, de longe, a exceção mais comum dentro do ecossistema do .NET. O criador de null
, Sir Tony Hoare, refere-se null
a como o "erro de um bilhão de dólares".
No exemplo a seguir, a variável FooBar
é atribuída a null
e imediatamente desreferenciada, exibindo o problema:
// Declare variable and assign it as null.
FooBar fooBar = null;
// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();
// The FooBar type definition.
record FooBar(int Id, string Name);
O problema se torna muito mais difícil de identificar como desenvolvedor quando seus aplicativos crescem em tamanho e complexidade. Identificar possíveis erros como esse é um trabalho para ferramentas e o compilador do C# está aqui para ajudar.
Definindo a segurança nula
O termo segurança nula define um conjunto de recursos específicos para tipos que permitem valor nulo que ajudam a reduzir o número de ocorrências NullReferenceException
possíveis.
Considerando o exemplo FooBar
anterior, você pode evitar o NullReferenceException
verificando se a variável fooBar
era null
antes de desreferenciá-la:
// Declare variable and assign it as null.
FooBar fooBar = null;
// Check for null
if (fooBar is not null)
{
_ = fooBar.ToString();
}
// The FooBar type definition for example.
record FooBar(int Id, string Name);
Para ajudar a identificar cenários como esse, o compilador pode inferir a intenção do código e impor o comportamento desejado. No entanto, isso ocorre somente quando um contexto que permite valor nulo é habilitado. Antes de discutir o contexto que permite valor nulo, vamos descrever os possíveis tipos que permitem valor nulo.
Tipos anuláveis
Antes do C# 2.0, somente os tipos de referência eram anuláveis. Os tipos de valor como int
ou DateTime
não podem ser null
. Se esses tipos são inicializados sem um valor, eles fazem fallback para seu valor default
. No caso de um int
, é 0
. Para um DateTime
, é DateTime.MinValue
.
Os tipos de referência instanciados sem valores iniciais funcionam de maneira diferente. O valor default
de todos os tipos de referência é null
.
Considere o trecho de C# a seguir:
string first; // first is null
string second = string.Empty // second is not null, instead it's an empty string ""
int third; // third is 0 because int is a value type
DateTime date; // date is DateTime.MinValue
No exemplo anterior:
first
énull
porque o tipo de referênciastring
foi declarado, mas nenhuma atribuição foi feita.second
é atribuídostring.Empty
quando é declarado. O objeto nunca teve uma atribuiçãonull
.third
é0
, apesar de não ter sido atribuído. É um (tipo-valor)struct
e tem um valordefault
de0
.date
tem a inicialização cancelada, mas o valordefault
dele é System.DateTime.MinValue.
A partir do C# 2.0, você pode definir tipos que permitem valor nulo usando Nullable<T>
(ou T?
para resumir). Isso permite que os tipos de valor sejam anuláveis. Considere o trecho de C# a seguir:
int? first; // first is implicitly null (uninitialized)
int? second = null; // second is explicitly null
int? third = default; // third is null as the default value for Nullable<Int32> is null
int? fourth = new(); // fourth is 0, since new calls the nullable constructor
No exemplo anterior:
first
énull
porque o tipo que permite valor nulo é não reinicializado.second
é atribuídonull
quando é declarado.third
énull
assim como o valordefault
paraNullable<int>
énull
.fourth
é0
assim como a expressãonew()
chama o construtorNullable<int>
eint
é0
por padrão.
O C# 8.0 introduziu tipos de referência que permitem valor nulo onde você pode expressar sua intenção de que um tipo de referência possa ser null
ou sempre não-null
. Você pode estar pensando: "Achei que os tipos de referência eram anuláveis". Você não está errado, e eles são. Esse recurso permite que você expresse sua intenção, que o compilador tenta impor. A mesma sintaxe T?
expressa que um tipo de referência deve ser anulável.
Considere o trecho de C# a seguir:
#nullable enable
string first = string.Empty;
string second;
string? third;
Considerando o exemplo anterior, o compilador infere sua intenção da seguinte forma:
first
nunca énull
como ele é definitivamente atribuído.second
nunca deve sernull
, mesmo que seja inicialmentenull
. Avaliandosecond
antes de atribuir um valor resulta em um aviso do compilador, pois ele é não inicializado.third
pode sernull
. Por exemplo, ele pode apontar para umSystem.String
, mas pode apontar paranull
. Qualquer uma dessas variações é aceitável. O compilador ajuda você com um aviso se você desreferenciarthird
sem primeiro verificar se ele não é nulo.
Importante
Para usar o recurso de tipos de referência que permitem valor nulo, conforme mostrado acima, ele deve estar dentro de um contexto que permite valor nulo. Isso é detalhado na próxima seção.
Contexto que permite valor nulo
Contextos que permitem valor nulo habilitam o controle refinado para a maneira como o compilador interpreta variáveis de tipo de referência. Há quatro contextos anuláveis possíveis:
disable
: o compilador se comporta da mesma forma que o C# 7.3 e anterior.enable
: o compilador habilita toda a análise de referência nula e todos os recursos de linguagem.warnings
: o compilador executa todas as análises nulas e emite avisos quando o código pode ser desreferenciadonull
.annotations
: o compilador não executa análise nula ou emite avisos quando o código pode ser desreferenciadonull
, mas você ainda pode anotar seu código usando tipos de referência anuláveis?
e operadores tolerantes a nulos (!
).
Este módulo tem como escopo os contextos anuláveis disable
ou enable
. Para obter mais informações, veja Tipos de referência anuláveis: Contextos anuláveis.
Habilitar tipos de referência anuláveis
No arquivo de projeto C# (.csproj), adicione um nó filho <Nullable>
ao elemento <Project>
(ou acrescente a um <PropertyGroup>
existente). Isso aplicará o contexto anulável enable
a todo o projeto.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Omitted for brevity -->
</Project>
Como alternativa, você pode fazer o escopo de contexto anulável para um arquivo C# usando uma diretiva de compilador.
#nullable enable
A diretiva de compilador C# anterior é funcionalmente equivalente à configuração do projeto, mas tem como escopo o arquivo no qual ele reside. Para obter mais informações, consulte Tipos de referência anuláveis: contextos anuláveis (documentação)
Importante
O contexto anulável é habilitado no arquivo .csproj por padrão em todos os modelos de projeto C#, começando com o .NET 6.0 e superior.
Quando o contexto anulável estiver habilitado, você receberá novos avisos. Considere o exemplo FooBar
anterior, que tem dois avisos quando analisado em um contexto anulável:
A linha
FooBar fooBar = null;
tem um aviso na atribuiçãonull
: Aviso de C# CS8600: conversão de literal nula ou possível valor nulo em tipo não anulável.A linha
_ = fooBar.ToString();
também tem um aviso. Desta vez, o compilador está preocupado quefooBar
pode ser nulo: Aviso de C# CS8602: desreferenciar uma referência possivelmente nula.
Importante
Não há segurança nula garantida, mesmo se você reagir e eliminar todos os avisos. Há alguns cenários limitados que passarão na análise do compilador, ainda que resultem em uma NullReferenceException
de runtime.
Resumo
Nesta unidade, você aprendeu a habilitar um contexto anulável em C# para ajudar a protegê-lo de NullReferenceException
. Na próxima unidade, você aprenderá mais sobre como expressar explicitamente sua intenção em um contexto anulável.