¿Por qué tenemos una posible advertencia de referencia nula de desreferencia, cuando la referencia nula no parece posible?

9

Habiendo leído esta pregunta en HNQ, seguí leyendo sobre los tipos de referencia anulables en C # 8 , e hice algunos experimentos.

Soy muy consciente de que 9 de cada 10 veces, o incluso más a menudo, cuando alguien dice "¡Encontré un error del compilador!" esto es en realidad por diseño y su propio malentendido. Y desde que comencé a analizar esta característica solo hoy, claramente no la entiendo muy bien. Con esto fuera del camino, veamos 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
    }
}

Después de leer la documentación que he vinculado anteriormente, esperaría que s == null línea me avisara, después de todo ses claramente no anulable, por lo que compararlo nullno tiene sentido.

En cambio, recibo una advertencia en la siguiente línea, y la advertencia dice que ses posible una referencia nula, aunque, para un humano, es obvio que no lo es.

Además, la advertencia no se muestra si no nos comparamos scon null.

Busqué en Google y me encontré con un problema de GitHub , que resultó ser sobre algo completamente diferente, pero en el proceso tuve una conversación con un colaborador que me dio más información sobre este comportamiento (por ejemplo, "Las comprobaciones nulas son a menudo una forma útil de decirle al compilador que restablezca su inferencia previa sobre la nulabilidad de una variable " . Sin embargo, esto todavía me dejó con la pregunta principal sin respuesta.

En lugar de crear un nuevo problema de GitHub, y potencialmente tomar el tiempo de los contribuyentes del proyecto increíblemente ocupados, lo estoy exponiendo a la comunidad.

¿Podría explicarme qué está pasando y por qué? En particular, ¿por qué no se generan advertencias en la s == nulllínea y por qué las tenemos CS8602cuando no parece que una nullreferencia sea posible aquí? Si la inferencia de nulabilidad no es a prueba de balas, como sugiere el hilo de GitHub vinculado, ¿cómo puede salir mal? ¿Cuáles serían algunos ejemplos de eso?

Andrew Savinykh
fuente
Parece que el compilador mismo establece un comportamiento que en este punto la variable "s" podría ser nula. De todos modos, si alguna vez uso cadenas u objetos, siempre debe haber una comprobación antes de llamar a una función. "s? .Length" debería hacer el truco y la advertencia en sí debería desaparecer.
chg
1
@chg, no debería ser necesario ?porque sno es anulable. No se anula, simplemente porque fuimos lo suficientemente tontos como para compararlo null.
Andrew Savinykh
Estaba siguiendo una pregunta anterior (lo siento, no puedo encontrarla) donde se postuló que si agrega una verificación de que un valor es nulo, entonces el compilador lo toma como 'pista' de que el valor podría ser nulo, incluso si eso Es demostrable que no es el caso.
stuartd
@stuartd, sí, eso es lo que parece ser. Entonces, la pregunta es: ¿por qué es útil?
Andrew Savinykh
1
@chg, bueno, eso es lo que digo en el cuerpo de la pregunta, ¿no?
Andrew Savinykh

Respuestas:

7

Esto es efectivamente un duplicado de la respuesta que @stuartd vinculó, por lo que no voy a entrar en detalles súper profundos aquí. Pero la raíz del asunto es que no se trata de un error de idioma ni de un compilador, sino de un comportamiento previsto tal como se implementó. Rastreamos el estado nulo de una variable. Cuando declara inicialmente la variable, ese estado es NotNull porque lo inicializa explícitamente con un valor que no es nulo. Pero no rastreamos de dónde vino ese NotNull. Esto, por ejemplo, es efectivamente un 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
    }
}

En ambos casos, se prueba de forma explícita spara null. Tomamos esto como entrada para el análisis de flujo, tal como Mads respondió en esta pregunta: https://stackoverflow.com/a/59328672/2672518 . En esa respuesta, el resultado es que recibe una advertencia en la devolución. En este caso, la respuesta es que recibe una advertencia de que ha desreferenciado una referencia posiblemente nula.

No se anula, simplemente porque fuimos lo suficientemente tontos como para compararlo null.

Sí, en realidad lo hace. Al compilador. Como humanos, podemos mirar este código y obviamente entender que no puede arrojar una excepción de referencia nula. Pero la forma en que se implementa el análisis de flujo anulable en el compilador, no puede. Discutimos algunas mejoras en este análisis donde agregamos estados adicionales basados ​​en el origen del valor, pero decidimos que esto agregaba una gran complejidad a la implementación sin mucha ganancia, porque los únicos lugares donde Esto sería útil para casos como este, en los que el usuario inicializa una variable con un newvalor constante o luego y luego la verifica de nulltodos modos.

333fred
fuente
Gracias. Esto aborda principalmente las similitudes con la otra pregunta, me gustaría abordar más las diferencias. Por ejemplo, ¿por qué s == nullno produce una advertencia?
Andrew Savinykh
También me di cuenta de eso con #nullable enable; string s = "";s = null;compila y funciona (todavía produce una advertencia) ¿cuáles son los beneficios de la implementación que permite asignar nulo a una "referencia no anulable" en un contexto de anulación nulo habilitado?
Andrew Savinykh
La respuesta de Mad se centra en el hecho de que "[el compilador] no rastrea la relación entre el estado de variables separadas" no tenemos variables separadas en este ejemplo, por lo que tengo problemas para aplicar el resto de la respuesta de Mad a este caso.
Andrew Savinykh
No hace falta decirlo, pero recuerden, estoy aquí no para criticar, sino para aprender. He estado usando C # desde el momento en que salió por primera vez en 2001. Aunque no soy nuevo en el lenguaje, la forma en que se comporta el compilador me sorprendió. El objetivo de esta pregunta es apoyar la razón por la cual este comportamiento es útil para los humanos.
Andrew Savinykh
Hay razones válidas para verificar s == null. Quizás, por ejemplo, estás en un método público y quieres hacer la validación de parámetros. O, tal vez esté utilizando una biblioteca que anotó incorrectamente, y hasta que solucionen ese error, debe lidiar con nulo donde no se declaró. En cualquiera de esos casos, si advertimos, sería una mala experiencia. En cuanto a permitir la asignación: las anotaciones de variables locales son solo para leer. No afectan el tiempo de ejecución en absoluto. De hecho, ponemos todas esas advertencias en un único código de error para que pueda desactivarlas si desea reducir la pérdida de código.
333fred
0

Si la inferencia de nulabilidad no es a prueba de balas, [..] ¿cómo puede salir mal?

Felizmente adopté las referencias anulables de C # 8 tan pronto como estuvieron disponibles. Como estaba acostumbrado a usar la notación [NotNull] (etc.) de ReSharper, noté algunas diferencias entre los dos.

El compilador de C # puede ser engañado, pero tiende a errar por precaución (generalmente, no siempre).

Como referencia para futuros visitantes, estos son los escenarios por los que vi que el compilador estaba bastante confundido (supongo que todos estos casos son por diseño):

  • Nulo perdonador nulo . A menudo se usa para evitar la advertencia de desreferencia, pero manteniendo el objeto no anulable. Parece querer mantener el pie en dos zapatos.
    string s = null!; //No warning

  • Análisis de superficie . En oposición a ReSharper (que lo hace usando la anotación de código ), el compilador de C # todavía no admite una gama completa de atributos para manejar las referencias anulables.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Sin embargo, permite usar alguna construcción para verificar la nulabilidad que también elimina la advertencia:

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

o atributos (aún geniales) (búscalos aquí ):

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

  • Análisis de código susceptible . Esto es lo que trajiste a la luz. El compilador tiene que hacer suposiciones para funcionar y, a veces, pueden parecer contra-intuitivas (al menos para los humanos).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Problemas con los genéricos . Preguntado aquí y explicado muy bien aquí (mismo artículo que antes, párrafo "¿El problema con T?"). Los genéricos son complicados ya que tienen que hacer felices tanto las referencias como los valores. La principal diferencia es que si bien string?es solo una cadena, se int?convierte en una Nullable<int>y obliga al compilador a manejarlas de maneras sustancialmente diferentes. También aquí, el compilador está eligiendo la ruta segura, lo que le obliga a especificar lo que espera:
    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.
    }

Resuelto dando restricciones:

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

Pero si no usamos restricciones y eliminamos el '?' de datos, todavía podemos poner valores nulos en él usando la palabra clave 'predeterminada':

    [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.

El último me parece más complicado, ya que permite escribir código inseguro.

Espero que esto ayude a alguien.

Alvin Sartor
fuente
Sugeriría leer los documentos sobre atributos anulables: docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Resolverán algunos de sus problemas, particularmente con la sección de análisis de superficie y genéricos.
333fred
Gracias por el enlace @ 333fred. Aunque hay atributos con los que puedes jugar , no existe un atributo que resuelva el problema que publiqué (algo que me dice que ThrowIfNull(s);me asegura que sno es nulo). Además, el artículo explica cómo manejar genéricos no anulables , mientras mostraba cómo puede "engañar" al compilador, teniendo un valor nulo pero sin advertencia al respecto.
Alvin Sartor
En realidad, el atributo existe. Archivé un error en los documentos para agregarlo. Usted está buscando DoesNotReturnIf(bool).
333fred
@ 333fred en realidad estoy buscando algo más parecido DoesNotReturnIfNull(nullable).
Alvin Sartor