Por que obtemos um possível aviso de referência nula de desreferência, quando a referência nula não parece ser possível?

9

Depois de ler esta pergunta no HNQ, passei a ler sobre tipos de referência nulos no C # 8 e fiz algumas experiências.

Estou ciente de que 9 vezes em 10, ou ainda mais frequentemente, quando alguém diz "Encontrei um bug do compilador!" isso é realmente por design e por seu próprio mal-entendido. E desde que comecei a examinar esse recurso somente hoje, claramente não tenho um entendimento muito bom dele. Com isso fora do caminho, vamos olhar para este código:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Depois de ler a documentação a que vinculei acima, esperaria que a s == nulllinha me desse um aviso - afinal de contas sé claramente não nulo, portanto, compará-la com nullnão faz sentido.

Em vez disso, estou recebendo um aviso na próxima linha, e o aviso diz que sé possível uma referência nula, mesmo que, para um humano, seja óbvio que não.

Além disso, o aviso não será exibido se não compararmos scom null.

Eu pesquisei no Google e encontrei um problema do GitHub , que acabou por ser sobre algo totalmente diferente, mas no processo tive uma conversa com um colaborador que forneceu mais informações sobre esse comportamento (por exemplo, "Verificações nulas geralmente são uma maneira útil de dizer ao compilador para redefinir sua inferência anterior sobre a nulidade de uma variável. " ). Isso ainda me deixou com a pergunta principal sem resposta, no entanto.

Em vez de criar um novo problema no GitHub e potencialmente ocupar o tempo dos colaboradores incrivelmente ocupados do projeto, estou divulgando isso para a comunidade.

Poderia me explicar o que está acontecendo e por quê? Em particular, por que nenhum aviso é gerado na s == nulllinha e por que temos CS8602quando parece que uma nullreferência não é possível aqui? Se a inferência de nulidade não é à prova de balas, como sugere o encadeamento do GitHub vinculado, como isso pode dar errado? Quais seriam alguns exemplos disso?

Andrew Savinykh
fonte
Parece que o próprio compilador define um comportamento que, neste ponto, a variável "s" poderia ser nula. De qualquer forma, se eu usar seqüências de caracteres ou objetos, sempre haverá uma verificação antes de você chamar uma função. "s? .Length" deve fazer o truque e o aviso em si deve desaparecer.
chg
11
@chg, não deve haver necessidade, ?porque snão é anulável. Não se torna anulável, simplesmente porque éramos tolos o suficiente para compará-lo null.
Andrew Savinykh
Eu estava seguindo uma pergunta anterior (desculpe, mas não consigo encontrá-la), onde foi postulado que, se você adicionar uma verificação de que um valor é nulo, o compilador considera isso como 'dica' de que o valor pode ser nulo, mesmo que comprovadamente não é o caso.
Stuartd
@ Stuartd, sim, é isso que parece. Então agora a pergunta é: por que isso é útil?
Andrew Savinykh
11
@ chg, bem, é o que eu digo no corpo da pergunta, não é?
Andrew Savinykh

Respostas:

7

Esta é efetivamente uma duplicata da resposta que o @stuartd vinculou, então não vou entrar em detalhes super profundos aqui. Mas a raiz do problema é que não se trata de um erro de linguagem nem de um compilador, mas seu comportamento pretendido exatamente como implementado. Nós rastreamos o estado nulo de uma variável. Quando você declara inicialmente a variável, esse estado é NotNull porque você a inicializa explicitamente com um valor que não é nulo. Mas não rastreamos de onde esse NotNull veio. Este, por exemplo, é efetivamente um código equivalente:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Em ambos os casos, você testar explicitamente spara null. Tomamos isso como entrada para a análise de fluxo, assim como Mads respondeu nesta pergunta: https://stackoverflow.com/a/59328672/2672518 . Nessa resposta, o resultado é que você recebe um aviso sobre o retorno. Nesse caso, a resposta é que você recebe um aviso de que não referenciou uma referência possivelmente nula.

Não se torna anulável, simplesmente porque éramos tolos o suficiente para compará-lo null.

Sim, ele realmente faz. Para o compilador. Como humanos, podemos olhar para este código e, obviamente, entender que ele não pode lançar uma exceção de referência nula. Mas a maneira como a análise de fluxo anulável é implementada no compilador, não pode. Discutimos algumas melhorias nesta análise, onde adicionamos estados adicionais com base na origem do valor, mas decidimos que isso adicionava uma grande complexidade à implementação, sem muito ganho, porque os únicos lugares onde isso seria útil em casos como este, em que o usuário inicializa uma variável com um newvalor constante ou constante e a verifica de nullqualquer maneira.

333fred
fonte
Obrigado. Isso aborda principalmente semelhanças com a outra pergunta, eu gostaria de abordar mais as diferenças. Por exemplo, por s == nullque não produz um aviso?
Andrew Savinykh
Também acabei de perceber isso com #nullable enable; string s = "";s = null;compila e trabalha (ainda produz um aviso) quais são os benefícios da implementação que permite atribuir nulo a uma "referência não anulável" no contexto de anotação nula ativado?
Andrew Savinykh
A resposta de Mad se concentra no fato de que "[compilador] não rastreia o relacionamento entre o estado de variáveis ​​separadas", não temos variáveis ​​separadas neste exemplo, portanto, tenho problemas para aplicar o restante da resposta de Mad nesse caso.
Andrew Savinykh
Escusado será dizer, mas lembre-se, não estou aqui para criticar, mas para aprender. Eu uso C # desde que foi lançado em 2001. Mesmo não sendo novo no idioma, o modo como o compilador se comporta era surpreendente para mim. O objetivo desta pergunta é basear-se na lógica de por que esse comportamento é útil para os seres humanos.
Andrew Savinykh
Há razões válidas para verificar s == null. Talvez, por exemplo, você esteja em um método público e deseje validar parâmetros. Ou talvez você esteja usando uma biblioteca que anotou incorretamente e, até que consertem esse bug, você precisará lidar com nulo onde não foi declarado. Em qualquer um desses casos, se avisássemos, seria uma experiência ruim. Quanto à permissão de atribuição: as anotações de variáveis ​​locais são apenas para leitura. Eles não afetam o tempo de execução. De fato, colocamos todos esses avisos em um único código de erro, para que você possa desativá-los se desejar reduzir a rotatividade do código.
333fred
0

Se a inferência de nulidade não é à prova de balas, [..] como isso pode dar errado?

Felizmente, adotei as referências anuláveis ​​do C # 8 assim que disponíveis. Como eu costumava usar a notação [NotNull] (etc.) do ReSharper, notei algumas diferenças entre os dois.

O compilador C # pode ser enganado, mas tende a errar por precaução (geralmente, nem sempre).

Como referência para futuros visitantes, esses são os cenários dos quais vi o compilador bastante confuso (presumo que todos esses casos sejam projetados):

  • Nulo perdão nulo . Geralmente usado para evitar o aviso de desreferência, mas mantendo o objeto não anulável. Parece querer manter o pé em dois sapatos.
    string s = null!; //No warning

  • Análise de superfície . Ao contrário do ReSharper (que faz isso usando anotação de código ), o compilador C # ainda não suporta uma gama completa de atributos para manipular as referências anuláveis.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

No entanto, permite usar alguma construção para verificar a nulidade que também se livra do aviso:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

ou atributos (ainda bem legais) (encontre todos aqui ):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Análise de código suscetível . Isto é o que você trouxe à luz. O compilador precisa fazer suposições para funcionar e, às vezes, elas podem parecer contra-intuitivas (pelo menos para os humanos).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Problemas com genéricos . Perguntado aqui e explicado muito bem aqui (mesmo artigo de antes, parágrafo "O problema com T?"). Os genéricos são complicados, pois precisam fazer referências e valores felizes. A principal diferença é que, enquanto string?é apenas uma string, int?torna-se um Nullable<int>e força o compilador a lidar com eles de maneiras substancialmente diferentes. Também aqui, o compilador está escolhendo o caminho seguro, forçando você a especificar o que está esperando:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Restrições de doação resolvidas:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

Mas se não usarmos restrições e removermos o '?' de Data, ainda podemos inserir valores nulos usando a palavra-chave 'default':

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

O último parece o mais complicado para mim, pois permite escrever código inseguro.

Espero que isso ajude alguém.

Alvin Sartor
fonte
Sugiro que você leia os documentos sobre atributos anuláveis: docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Eles resolverão alguns dos seus problemas, principalmente na seção de análise de superfície e genéricos.
333fred
Obrigado pelo link @ 333fred. Embora haja atributos com os quais você possa brincar, não existe um atributo que resolva o problema que publiquei (algo que me diz que ThrowIfNull(s);está me assegurando que snão é nulo). Além disso, o artigo explica como manipular genéricos não anuláveis , enquanto eu mostrava como você pode "enganar" o compilador, tendo um valor nulo, mas sem aviso.
Alvin Sartor
Na verdade, o atributo existe. Eu registrei um bug nos documentos para adicioná-lo. Você está procurando DoesNotReturnIf(bool).
333fred
@ 333fred, na verdade, estou procurando algo mais parecido DoesNotReturnIfNull(nullable).
Alvin Sartor