Posso adivinhar razoavelmente o que está acontecendo aqui, mas é um pouco complicado :) Envolve o estado nulo e o rastreamento nulo descritos no rascunho da especificação . Fundamentalmente, no ponto em que queremos retornar, o compilador avisará se o estado da expressão for "talvez nulo" em vez de "não nulo".
Esta resposta é de certa forma narrativa, e não apenas "aqui estão as conclusões" ... Espero que seja mais útil dessa maneira.
Vou simplificar um pouco o exemplo, livrando-me dos campos e considere um método com uma dessas duas assinaturas:
public static string M(string? text)
public static string M(string text)
Nas implementações abaixo, dei a cada método um número diferente para que eu possa consultar exemplos específicos sem ambiguidade. Ele também permite que todas as implementações estejam presentes no mesmo programa.
Em cada um dos casos descritos abaixo, faremos várias coisas, mas acabamos tentando retornar text
- por isso é importante o estado nulo text
.
Retorno incondicional
Primeiro, vamos tentar devolvê-lo diretamente:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
Até agora, tão simples. O estado anulável do parâmetro no início do método é "talvez nulo" se for do tipo string?
e "não nulo" se for do tipo string
.
Retorno condicional simples
Agora vamos verificar se há nulo na if
própria condição da instrução. (Eu usaria o operador condicional, que acredito ter o mesmo efeito, mas queria permanecer mais fiel à questão.)
public static string M3(string? text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
public static string M4(string text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
Ótimo, portanto, dentro de uma if
instrução em que a própria condição verifica a nulidade, o estado da variável em cada ramo da if
instrução pode ser diferente: dentro do else
bloco, o estado "não é nulo" em ambas as partes do código. Assim, em particular, no M3 o estado muda de "talvez nulo" para "não nulo".
Retorno condicional com uma variável local
Agora vamos tentar elevar essa condição a uma variável local:
public static string M5(string? text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
public static string M6(string text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
Ambos M5 e M6 avisos. Portanto, não apenas não obtemos o efeito positivo da mudança de estado de "talvez nulo" para "não nulo" no M5 (como fizemos no M3) ... obtemos o efeito oposto no M6, de onde o estado passa " não nulo "a" talvez nulo ". Isso realmente me surpreendeu.
Parece que aprendemos isso:
- A lógica em torno de "como uma variável local foi calculada" não é usada para propagar informações de estado. Mais sobre isso mais tarde.
- A introdução de uma comparação nula pode avisar o compilador de que algo que antes pensava não ser nulo poderia ser nulo, afinal.
Retorno incondicional após uma comparação ignorada
Vejamos o segundo desses pontos, introduzindo uma comparação antes de um retorno incondicional. (Portanto, estamos ignorando completamente o resultado da comparação.):
public static string M7(string? text)
{
bool ignored = text is null;
return text; // Warning
}
public static string M8(string text)
{
bool ignored = text is null;
return text; // Warning
}
Observe como M8 parece que deve ser equivalente a M2 - ambos têm um parâmetro não nulo que retornam incondicionalmente - mas a introdução de uma comparação com nulo altera o estado de "não nulo" para "talvez nulo". Podemos obter mais evidências disso tentando remover a referência text
antes da condição:
public static string M9(string text)
{
int length1 = text.Length; // No warning
bool ignored = text is null;
int length2 = text.Length; // Warning
return text; // No warning
}
Observe como a return
instrução não possui um aviso agora: o estado após a execução text.Length
é "não nulo" (porque se executarmos essa expressão com sucesso, ela não poderá ser nula). Portanto, o text
parâmetro começa como "não nulo" devido ao seu tipo, torna-se "talvez nulo" devido à comparação nula e depois torna-se "não nulo" novamente depois text2.Length
.
Quais comparações afetam o estado?
Então isso é uma comparação de text is null
... que efeito as comparações semelhantes têm? Aqui estão mais quatro métodos, todos começando com um parâmetro de sequência não anulável:
public static string M10(string text)
{
bool ignored = text == null;
return text; // Warning
}
public static string M11(string text)
{
bool ignored = text is object;
return text; // No warning
}
public static string M12(string text)
{
bool ignored = text is { };
return text; // No warning
}
public static string M13(string text)
{
bool ignored = text != null;
return text; // Warning
}
Portanto, embora x is object
agora seja uma alternativa recomendada x != null
, eles não têm o mesmo efeito: somente uma comparação com null (com qualquer um de is
, ==
ou !=
) altera o estado de "not null" para "maybe null".
Por que içar a condição tem efeito?
Voltando ao nosso primeiro marcador, por que M5 e M6 não levam em consideração a condição que levou à variável local? Isso não me surpreende tanto quanto parece surpreender os outros. Construir esse tipo de lógica no compilador e na especificação é muito trabalhoso e com relativamente pouco benefício. Aqui está outro exemplo que não tem nada a ver com a nulidade, em que algo embutido tem efeito:
public static int X1()
{
if (true)
{
return 1;
}
}
public static int X2()
{
bool alwaysTrue = true;
if (alwaysTrue)
{
return 1;
}
// Error: not all code paths return a value
}
Mesmo sabendo que isso alwaysTrue
sempre será verdade, ele não satisfaz os requisitos da especificação que tornam o código após a if
instrução inacessível, e é disso que precisamos.
Aqui está outro exemplo, em torno de atribuição definida:
public static void X3()
{
string x;
bool condition = DateTime.UtcNow.Year == 2020;
if (condition)
{
x = "It's 2020.";
}
if (!condition)
{
x = "It's not 2020.";
}
// Error: x is not definitely assigned
Console.WriteLine(x);
}
Mesmo que nós sabemos que o código vai entrar exatamente um daqueles if
corpos declaração, não há nada na especificação de trabalho que fora. As ferramentas de análise estática podem muito bem fazê-lo, mas tentar colocar isso na especificação da linguagem seria uma má idéia, IMO - é bom que as ferramentas de análise estática tenham todos os tipos de heurísticas que podem evoluir ao longo do tempo, mas não tanto. para uma especificação de idioma.