¿Por qué no recibo una advertencia sobre la posible desreferencia de un valor nulo en C # 8 con un miembro de clase de una estructura?

8

En un proyecto C # 8 con tipos de referencia anulables habilitados, tengo el siguiente código que creo que debería darme una advertencia sobre una posible desreferencia nula, pero no lo hace:

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
    }
}

Cuando instancese inicializa con el constructor predeterminado, instance.Memberse establece en el valor predeterminado de ExampleClassMember, que es null. Por lo tanto, instance.Member.Valuelanzará un NullReferenceExceptionen tiempo de ejecución. Como entiendo la detección de nulabilidad de C # 8, debería recibir una advertencia del compilador sobre esta posibilidad, pero no lo hago; ¿porqué es eso?

DylanSp
fuente
¿Has presentado esto como un problema en el repositorio de Roslyn GitHub?
Dai
@Dai no lo he hecho (todavía); si es un error legítimo y no es algo que me falta, lo haré.
DylanSp
FWIW, este código no se compila en C # 7.0: obtengo un error sobre los dos tipos que carecen de constructores para establecer los valores de las propiedades automáticas. Sin embargo, se compila con los compiladores Roslyn 3.0 y .NET Core 3.0, y de hecho se ejecuta con un NRE en ambos casos. Sin embargo, estoy usando un IDE basado en la web sin la capacidad de establecer opciones de compilación.
Dai
El compilador de C # 8.0 me advierte cuando cambio ExampleStructde structa class.
Dai
1
@tymtam que es para una versión preliminar. En la versión de lanzamiento, esNullable
Panagiotis Kanavos

Respuestas:

13

Tenga en cuenta que no hay razón para que haya una advertencia en la llamada Console.WriteLine(). La propiedad de tipo de referencia no es un tipo anulable, por lo que no es necesario que el compilador advierta que puede ser nulo.

Podría argumentar que el compilador debería advertir sobre la referencia en structsí misma. Eso me parecería razonable. Pero no lo hace. Esto parece ser un vacío legal, causado por la inicialización predeterminada para los tipos de valor, es decir, siempre debe haber un constructor predeterminado (sin parámetros), que siempre pone a cero todos los campos (nulos para los campos de tipo de referencia, ceros para los tipos numéricos, etc.) )

Lo llamo un vacío legal, porque en teoría los valores de referencia no anulables siempre deberían ser no nulos. Duh :)

Este vacío parece abordarse en este artículo del blog: Introducción de tipos de referencia anulables en C #

Evitar los valores nulos Hasta ahora, las advertencias se referían a proteger los valores nulos en referencias anulables para que no se desreferenciaran. El otro lado de la moneda es evitar tener nulos en las referencias no anulables.

Hay un par de formas en que los valores nulos pueden existir, y vale la pena advertir sobre la mayoría de ellos, mientras que un par de ellos causarían otro "mar de advertencias" que es mejor evitar:
...

  • Usar el constructor predeterminado de una estructura que tiene un campo de tipo de referencia no anulable. Este es astuto, ya que el constructor predeterminado (que pone a cero la estructura) incluso puede usarse implícitamente en muchos lugares. Probablemente sea mejor no advertir [énfasis mío - PD] , o de lo contrario muchos tipos de estructuras existentes se volverían inútiles.

En otras palabras, sí, esto es un vacío legal, pero no, no es un error. Los diseñadores de lenguaje lo saben, pero han optado por dejar este escenario fuera de las advertencias, porque no sería práctico hacerlo de otra manera dada la forma en que structfunciona la inicialización.

Tenga en cuenta que esto también está de acuerdo con la filosofía más amplia detrás de la función. Del mismo artículo:

Por lo tanto, queremos quejarse de su código existente. Pero no de manera desagradable. Así es como vamos a tratar de lograr ese equilibrio:
...

  1. No hay seguridad nula garantizada [énfasis mío - PD] , incluso si reacciona y elimina todas las advertencias. Hay muchos agujeros en el análisis por necesidad, y también algunos por elección.

Para ese último punto: a veces, una advertencia es lo "correcto", pero se dispararía todo el tiempo sobre el código existente, incluso cuando en realidad está escrito de una manera nula y segura. En tales casos, nos equivocaremos por conveniencia, no por corrección. No podemos estar generando un "mar de advertencias" en el código existente: demasiadas personas simplemente desactivan las advertencias y nunca se benefician de ello.

También tenga en cuenta que este mismo problema existe con las matrices de tipos de referencia nominalmente no anulables (por ejemplo string[]). Cuando crea la matriz, todos los valores de referencia son nully, sin embargo, esto es legal y no generará ninguna advertencia.


Demasiado para explicar por qué las cosas son como son. Entonces la pregunta es, ¿qué hacer al respecto? Eso es mucho más subjetivo, y no creo que haya una respuesta correcta o incorrecta. Dicho eso ...

Yo personalmente trataría mis structtipos caso por caso. Para aquellos en los que la intención es en realidad un tipo de referencia anulable, aplicaría la ?anotación. De lo contrario, no lo haría.

Técnicamente, cada valor de referencia individual en a structdebe ser "anulable", es decir, incluir la ?anotación anulable con el nombre del tipo. Pero al igual que con muchas características similares (como async / await en C # o consten C ++), esto tiene un aspecto "infeccioso", ya que deberá anular esa anotación más adelante (con la !anotación) o incluir una comprobación nula explícita , o solo asigne ese valor a otra variable de tipo de referencia anulable.

Para mí, esto anula mucho el propósito de habilitar tipos de referencia anulables. Dado que tales miembros de structtipos requerirán un manejo de casos especiales en algún momento de todos modos, y dado que la única forma de manejarlo de manera segura y al mismo tiempo poder usar tipos de referencia no anulables es poner cheques nulos en todos los lugares donde use struct, creo que es una opción de implementación razonable aceptar que cuando el código se inicializa struct, es responsabilidad del código hacerlo correctamente y asegurarse de que el miembro del tipo de referencia no anulable se inicialice de hecho a un valor no nulo.

Esto se puede ayudar proporcionando un medio de inicialización "oficial", como un constructor no predeterminado (es decir, uno con parámetros) o un método de fábrica. Siempre existirá el riesgo de usar el constructor predeterminado, o ningún constructor (como en las asignaciones de matrices), pero al proporcionar un medio conveniente para inicializar structcorrectamente, esto fomentará el código que lo usa para evitar referencias nulas en variables anulables.

Dicho esto, si lo que quiere es 100% de seguridad con respecto a los tipos de referencia anulables, entonces claramente el enfoque correcto para ese objetivo en particular es anotar siempre cada miembro de tipo de referencia en un structcon ?. Esto significa que cada campo y cada propiedad implementada automáticamente, junto con cualquier método o captador de propiedades que devuelva directamente dichos valores o el producto de dichos valores. Entonces, el código de consumo deberá incluir comprobaciones nulas o el operador que perdona los valores nulos en cada punto donde dichos valores se copien en variables no anulables.

Peter Duniho
fuente
Buen análisis, y gracias por encontrar esa publicación de blog, es una respuesta bastante concluyente.
DylanSp
1

A la luz de la excelente respuesta de @ peter-duniho, parece que a partir de octubre de 2019 es mejor marcar a todos los miembros que no sean de valor como una referencia anulable.

#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
    }
}
tymtam
fuente