Por que não recebo um aviso sobre possível desreferência de um nulo no C # 8 com um membro da classe de uma estrutura?

8

Em um projeto C # 8 com tipos de referência anuláveis ativados, tenho o seguinte código que acho que deveria me dar um aviso sobre uma possível desreferência nula, mas não:

public class ExampleClassMember
{
    public int Value { get; }
}

public struct ExampleStruct
{
    public ExampleClassMember Member { get; }
}

public class Program
{
    public static void Main(string[] args)
    {
        var instance = new ExampleStruct();
        Console.WriteLine(instance.Member.Value);  // expected warning here about possible null dereference
    }
}

Quando instanceé inicializado com o construtor padrão, instance.Memberé definido como o valor padrão de ExampleClassMember, que é null. Assim, instance.Member.Valuelançará um NullReferenceExceptionem tempo de execução. Como eu entendo a detecção de nulidade do C # 8, devo receber um aviso do compilador sobre essa possibilidade, mas não o faço; por que é que?

DylanSp
fonte
Você registrou isso como um problema no repositório do Roslyn GitHub?
Dai
@Dai ainda não; se for um bug legítimo e não algo que esteja faltando, eu irei.
DylanSp 17/10/19
FWIW, esse código não é compilado no C # 7.0 - recebo um erro sobre os dois tipos que não possuem construtores para definir os valores das propriedades automáticas. Porém, ele compila com os compiladores Roslyn 3.0 e .NET Core 3.0 e, na verdade, é executado com um NRE nos dois últimos casos. Estou usando um IDE baseado na Web sem a capacidade de definir opções do compilador.
Dai
O compilador C # 8.0 me avisa quando mudo ExampleStructde structpara class.
Dai
1
@tymtam é para uma versão prévia. Na versão de lançamento, é #Nullable
Panagiotis Kanavos

Respostas:

13

Observe que não há motivo para que haja um aviso na chamada para Console.WriteLine(). A propriedade do tipo de referência não é um tipo anulável e, portanto, não é necessário que o compilador avise que pode ser nulo.

Você pode argumentar que o compilador deve avisar sobre a referência em structsi. Isso me pareceria razoável. Mas isso não acontece. Isso parece ser uma brecha, causada pela inicialização padrão para tipos de valor, ou seja, sempre deve haver um construtor padrão (sem parâmetros), que sempre zera todos os campos (nulos para campos de tipo de referência, zeros para tipos numéricos etc.) )

Eu chamo isso de brecha, porque, em teoria, os valores de referência não nulos devem, de fato, ser sempre nulos! Duh. :)

Esta brecha parece ser abordada neste artigo do blog: Apresentando tipos de referência nulos em C #

Evitando nulos Até agora, os avisos eram sobre a proteção de nulos em referências anuláveis ​​de serem desreferenciadas. O outro lado da moeda é evitar ter nulos nas referências não anuláveis.

Existem algumas maneiras pelas quais os valores nulos podem surgir, e a maioria deles vale a pena alertar, enquanto alguns deles causariam outro “mar de avisos” que é melhor evitar:

  • Usando o construtor padrão de uma estrutura que possui um campo de tipo de referência não anulável. Este é sorrateiro, já que o construtor padrão (que zera a estrutura) pode até ser usado implicitamente em muitos lugares. Provavelmente é melhor não avisar [grifo meu - PD] , ou então muitos tipos de estruturas existentes seriam inúteis.

Em outras palavras, sim, isso é uma brecha, mas não, não é um bug. Os designers de idiomas estão cientes disso, mas optaram por deixar esse cenário de fora dos avisos, porque fazer o contrário seria impraticável, dada a maneira como a structinicialização funciona.

Observe que isso também está de acordo com a filosofia mais ampla por trás do recurso. Do mesmo artigo:

Então, queremos que ele se queixe do seu código existente. Mas não desagradável. Eis como vamos tentar encontrar esse equilíbrio:

  1. Não há segurança nula garantida [ênfase minha - PD] , mesmo que você reaja e elimine todos os avisos. Existem muitos buracos na análise por necessidade e outros por opção.

Até o último ponto: Às vezes, um aviso é a coisa "correta" a ser executada, mas dispara o tempo todo no código existente, mesmo quando ele é realmente escrito de maneira nula e segura. Nesses casos, erraremos por conveniência, não por correção. Não podemos estar produzindo um “mar de avisos” no código existente: muitas pessoas simplesmente desativariam os avisos e nunca se beneficiariam disso.

Observe também que esse mesmo problema existe com matrizes de tipos de referência nominalmente não nulos (por exemplo string[]). Quando você cria a matriz, todos os valores de referência são nulle, no entanto, isso é legal e não gera nenhum aviso.


Tanta coisa para explicar por que as coisas são do jeito que são. Então a pergunta se torna: o que fazer sobre isso? Isso é muito mais subjetivo, e não acho que haja uma resposta certa ou errada. Dito isto…

Eu pessoalmente trataria meus structtipos caso a caso. Para aqueles em que a intenção é realmente um tipo de referência anulável, eu aplicaria a ?anotação. Caso contrário, eu não faria.

Tecnicamente, todo valor de referência em um structdeve ser "anulável", ou seja, incluir a ?anotação anulável com o nome do tipo. Mas, como ocorre com muitos recursos semelhantes (como assíncrono / aguardar em C # ou constem C ++), isso tem um aspecto "infeccioso", pois você precisará substituir essa anotação posteriormente (com a !anotação) ou incluir uma verificação nula explícita , ou apenas atribua esse valor a outra variável de tipo de referência anulável.

Para mim, isso anula muito a finalidade de habilitar tipos de referência anuláveis. Como esses membros de structtipos exigirão tratamento de caso especial em algum momento, e como a única maneira de lidar com segurança com segurança e ainda poder usar tipos de referência não nulos é colocar verificações nulas em todos os lugares em que você usar struct, eu sinto que é uma opção de implementação razoável aceitar que, quando o código inicializa struct, é responsabilidade do código fazê-lo corretamente e garantir que o membro do tipo de referência não anulável seja realmente inicializado com um valor não nulo.

Isso pode ser auxiliado fornecendo um meio "oficial" de inicialização, como um construtor não padrão (por exemplo, um com parâmetros) ou um método de fábrica. Sempre haverá sempre o risco de usar o construtor padrão, ou nenhum construtor (como nas alocações de matriz), mas fornecendo um meio conveniente para inicializar o structcorreto, isso incentivará o código que o usa para evitar referências nulas em variáveis ​​anuláveis.

Dito isso, se o que você deseja é 100% de segurança em relação aos tipos de referência anuláveis, então claramente a abordagem correta para esse objetivo específico é sempre anotar todos os membros do tipo de referência em um structcom ?. Isso significa todos os campos e propriedades implementadas automaticamente, juntamente com qualquer método ou método de obtenção de propriedades que retorne diretamente esses valores ou o produto desses valores. Em seguida, o código de consumo precisará incluir verificações nulas ou o operador que perdoa nulas em todos os pontos em que esses valores são copiados em variáveis ​​não nulas.

Peter Duniho
fonte
Boa análise e obrigado por encontrar essa publicação no blog - é uma resposta bastante conclusiva.
DylanSp
1

À luz da excelente resposta de @ peter-duniho, parece que, a partir de outubro de 2019, é melhor marcar todos os membros que não são do tipo valor como uma referência nula.

#nullable enable
public class C
{
    public int P1 { get; } 
}

public struct S
{
    public C? Member { get; } // Reluctantly mark as nullable reference because
                              // https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/
                              // states:
                              // "Using the default constructor of a struct that has a
                              // field of nonnullable reference type. This one is 
                              // sneaky, since the default constructor (which zeroes 
                              // out the struct) can even be implicitly used in many
                              // places. Probably better not to warn, or else many
                              // existing struct types would be rendered useless."
}

public class Program
{
    public static void Main()
    {
        var instance = new S();
        Console.WriteLine(instance.Member.P1); // Warning
    }
}
timtim
fonte