Considere o seguinte código:
using System;
#nullable enable
namespace Demo
{
public sealed class TestClass
{
public string Test()
{
bool isNull = _test == null;
if (isNull)
return "";
else
return _test; // !!!
}
readonly string _test = "";
}
}
Quando eu construir este, a linha marcada com !!!
dá um aviso do compilador: warning CS8603: Possible null reference return.
.
Acho isso um pouco confuso, já que _test
é somente leitura e inicializado como não nulo.
Se eu alterar o código para o seguinte, o aviso desaparecerá:
public string Test()
{
// bool isNull = _test == null;
if (_test == null)
return "";
else
return _test;
}
Alguém pode explicar esse comportamento?
c#
nullable-reference-types
Matthew Watson
fonte
fonte
The Debug.Assert is irrelevant because that is a runtime check
- Ele é relevante porque se você comentar que fora de linha, o aviso vai embora.Debug.Assert
lançará uma exceção se o teste falhar.Debug.Assert
agora tem uma anotação ( src )DoesNotReturnIf(false)
para o parâmetro de condição.Respostas:
A análise de fluxo anulável rastreia o estado nulo das variáveis, mas não rastreia outro estado, como o valor de uma
bool
variável (comoisNull
acima), e não rastreia o relacionamento entre o estado de variáveis separadas (por exemplo,isNull
e_test
).Um mecanismo de análise estática real provavelmente faria essas coisas, mas também seria "heurístico" ou "arbitrário" até certo ponto: você não podia necessariamente dizer as regras que ele estava seguindo, e essas regras podem até mudar ao longo do tempo.
Isso não é algo que podemos fazer diretamente no compilador C #. As regras para avisos anuláveis são bastante sofisticadas (como mostra a análise de Jon!), Mas são regras e podem ser fundamentadas.
À medida que lançamos o recurso, parece que alcançamos o equilíbrio certo, mas há alguns lugares que parecem desajeitados e revisaremos os do C # 9.0.
fonte
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:
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 nulotext
.Retorno incondicional
Primeiro, vamos tentar devolvê-lo diretamente:
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 tipostring
.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.)Ó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 daif
instrução pode ser diferente: dentro doelse
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:
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:
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.):
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:Observe como a
return
instrução não possui um aviso agora: o estado após a execuçãotext.Length
é "não nulo" (porque se executarmos essa expressão com sucesso, ela não poderá ser nula). Portanto, otext
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 depoistext2.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:Portanto, embora
x is object
agora seja uma alternativa recomendadax != null
, eles não têm o mesmo efeito: somente uma comparação com null (com qualquer um deis
,==
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:
Mesmo sabendo que isso
alwaysTrue
sempre será verdade, ele não satisfaz os requisitos da especificação que tornam o código após aif
instrução inacessível, e é disso que precisamos.Aqui está outro exemplo, em torno de atribuição definida:
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.fonte
if (x != null) x.foo(); x.bar();
, temos duas evidências; aif
afirmação é evidência da proposição "o autor acredita que x pode ser nulo antes da chamada para foo" e a seguinte afirmação é evidência de "o autor acredita que x não é nula antes da chamada para barrar", e essa contradição leva ao conclusão de que existe um erro. O bug é o bug relativamente benigno de uma verificação nula desnecessária ou o bug potencialmente travando. Qual bug é o bug real não está claro, mas é claro que existe um.Você descobriu evidências de que o algoritmo de fluxo de programa que produz esse aviso é relativamente pouco sofisticado quando se trata de rastrear os significados codificados nas variáveis locais.
Não tenho conhecimento específico da implementação do verificador de fluxo, mas, tendo trabalhado em implementações de código semelhante no passado, posso fazer algumas suposições. O verificador de fluxo provavelmente deduz duas coisas no caso de falso positivo: (1)
_test
poderia ser nulo, porque, se não pudesse, você não teria a comparação em primeiro lugar e (2)isNull
poderia ser verdadeiro ou falso - porque se não pudesse, você não o teria em umif
. Mas a conexão que oreturn _test;
único executa se_test
não for nula, essa conexão não está sendo feita.Esse é um problema surpreendentemente complicado, e você deve esperar que o compilador leve um tempo para obter a sofisticação das ferramentas que tiveram vários anos de trabalho por especialistas. O verificador de fluxo Coverity, por exemplo, não teria nenhum problema em deduzir que nenhuma das duas variações teve um retorno nulo, mas o verificador de fluxo Coverity custa muito dinheiro para clientes corporativos.
Além disso, os verificadores de cobertura são projetados para rodar em grandes bases de código durante a noite ; a análise do compilador C # deve ser executada entre as teclas digitadas no editor , o que altera significativamente os tipos de análises detalhadas que você pode executar razoavelmente.
fonte
bool b = x != null
vsbool b = x is { }
(com nenhuma atribuição realmente usada!) mostra que mesmo os padrões reconhecidos para verificações nulas são questionáveis. Para não menosprezar o trabalho indiscutivelmente difícil da equipe de fazer com que esse trabalho funcione principalmente como deveria para bases de códigos reais e em uso - parece que a análise é pragmática com capital P.Todas as outras respostas estão praticamente exatamente corretas.
Caso alguém esteja curioso, tentei explicar a lógica do compilador da maneira mais explícita possível em https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947
A única parte que não é mencionada é como decidimos se uma verificação nula deve ser considerada "pura", no sentido de que, se você fizer isso, devemos considerar seriamente se nula é uma possibilidade. Há muitas verificações nulas "incidentais" no C #, nas quais você testa a nulidade como parte de outra ação, por isso decidimos que queríamos restringir o conjunto de verificações àquelas que tínhamos certeza de que as pessoas estavam fazendo deliberadamente. A heurística que surgiu foi "contém a palavra nula", então é por isso
x != null
ex is object
produzir resultados diferentes.fonte