Considere el siguiente 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 = "";
}
}
Cuando construyo esto, la línea marcada con !!!
da una advertencia del compilador: warning CS8603: Possible null reference return.
.
Esto me parece un poco confuso, dado que _test
es de solo lectura e inicializado como no nulo.
Si cambio el código a lo siguiente, la advertencia desaparece:
public string Test()
{
// bool isNull = _test == null;
if (_test == null)
return "";
else
return _test;
}
¿Alguien puede explicar este comportamiento?
c#
nullable-reference-types
Matthew Watson
fuente
fuente
The Debug.Assert is irrelevant because that is a runtime check
- Se es relevante porque si comentar que la línea de salida, la advertencia desaparece.Debug.Assert
arrojará una excepción si la prueba falla.Debug.Assert
ahora tiene una anotación ( src ) deDoesNotReturnIf(false)
para el parámetro de condición.Respuestas:
El análisis de flujo anulable rastrea el estado nulo de las variables, pero no rastrea otro estado, como el valor de una
bool
variable (comoisNull
arriba), y no rastrea la relación entre el estado de variables separadas (por ejemplo,isNull
y_test
).Un motor de análisis estático real probablemente haría esas cosas, pero también sería "heurístico" o "arbitrario" hasta cierto punto: no necesariamente podría decir las reglas que estaba siguiendo, y esas reglas podrían incluso cambiar con el tiempo.
Eso no es algo que podamos hacer directamente en el compilador de C #. Las reglas para las advertencias anulables son bastante sofisticadas (como lo muestra el análisis de Jon), pero son reglas y se pueden razonar.
A medida que implementamos la función, parece que en su mayoría alcanzamos el equilibrio correcto, pero hay algunos lugares que resultan incómodos, y volveremos a visitarlos para C # 9.0.
fuente
Puedo hacer una suposición razonable sobre lo que está sucediendo aquí, pero todo es un poco complicado :) Involucra el estado nulo y el seguimiento nulo descrito en el borrador de la especificación . Básicamente, en el punto donde queremos regresar, el compilador advertirá si el estado de la expresión es "quizás nulo" en lugar de "no nulo".
Esta respuesta está en forma narrativa en lugar de simplemente "aquí están las conclusiones" ... Espero que sea más útil de esa manera.
Voy a simplificar un poco el ejemplo al eliminar los campos y considerar un método con una de estas dos firmas:
En las implementaciones a continuación, le he dado a cada método un número diferente para poder referirme a ejemplos específicos sin ambigüedades. También permite que todas las implementaciones estén presentes en el mismo programa.
En cada uno de los casos descritos a continuación, haremos varias cosas, pero terminaremos intentando regresar
text
, por lo que es el estado nulo detext
que es importante el .Retorno incondicional
Primero, intentemos devolverlo directamente:
Hasta ahora, muy simple. El estado anulable del parámetro al comienzo del método es "tal vez nulo" si es de tipo
string?
y "no nulo" si es de tipostring
.Retorno condicional simple
Ahora verifiquemos si hay nulo dentro de la
if
condición de la declaración. (Usaría el operador condicional, que creo tendrá el mismo efecto, pero quería estar más fiel a la pregunta).Genial, por lo que parece dentro de una
if
declaración donde la condición misma verifica la nulidad, el estado de la variable dentro de cada rama de laif
declaración puede ser diferente: dentro delelse
bloque, el estado "no es nulo" en ambas partes del código. Entonces, en particular, en M3 el estado cambia de "quizás nulo" a "no nulo".Retorno condicional con una variable local
Ahora intentemos elevar esa condición a una variable local:
Tanto M5 como M6 emiten advertencias. Entonces, no solo no obtenemos el efecto positivo del cambio de estado de "quizás nulo" a "no nulo" en M5 (como lo hicimos en M3) ... obtenemos lo contrario efecto en M6, de dónde va el estado " no nulo "a" tal vez nulo ". Eso realmente me sorprendió.
Parece que hemos aprendido que:
Retorno incondicional después de una comparación ignorada
Veamos el segundo de esos puntos, introduciendo una comparación antes de un retorno incondicional. (Por lo tanto, estamos ignorando por completo el resultado de la comparación):
Tenga en cuenta cómo se siente que M8 debería ser equivalente a M2, ambos tienen un parámetro no nulo que devuelven incondicionalmente, pero la introducción de una comparación con nulo cambia el estado de "no nulo" a "quizás nulo". Podemos obtener más evidencia de esto al intentar desreferenciar
text
antes de la condición:Observe cómo la
return
declaración no tiene una advertencia ahora: el estado después de la ejecucióntext.Length
es "no nulo" (porque si ejecutamos esa expresión con éxito, no podría ser nula). Por lo tanto, eltext
parámetro comienza como "no nulo" debido a su tipo, se convierte en "quizás nulo" debido a la comparación nula, luego se vuelve "no nulo" nuevamentetext2.Length
.¿Qué comparaciones afectan al estado?
Entonces, esa es una comparación de
text is null
... ¿qué efecto tienen comparaciones similares? Aquí hay cuatro métodos más, todos comenzando con un parámetro de cadena no anulable:Entonces, aunque
x is object
ahora es una alternativa recomendadax != null
, no tienen el mismo efecto: solo una comparación con nulo (con cualquiera deis
,==
o!=
) cambia el estado de "no nulo" a "quizás nulo".¿Por qué alzar la condición tiene un efecto?
Volviendo a nuestro primer punto anterior, ¿por qué M5 y M6 no tienen en cuenta la condición que condujo a la variable local? Esto no me sorprende tanto como parece sorprender a los demás. Construir ese tipo de lógica en el compilador y la especificación es mucho trabajo y tiene relativamente poco beneficio. Aquí hay otro ejemplo que no tiene nada que ver con la nulabilidad en el que la alineación de algo tiene un efecto:
A pesar de que sabemos que
alwaysTrue
siempre habrá cierto, que no satisface los requisitos de la especificación que hacen que el código después de laif
declaración inalcanzable, que es lo que necesitamos.Aquí hay otro ejemplo, alrededor de la asignación definida:
A pesar de que sabemos que el código entrará exactamente uno de esos
if
cuerpos de los estados, no hay nada en la especificación de trabajo que fuera. Las herramientas de análisis estático pueden ser capaces de hacerlo, pero tratar de poner eso en la especificación del lenguaje sería una mala idea, en mi opinión: está bien que las herramientas de análisis estático tengan todo tipo de heurísticas que pueden evolucionar con el tiempo, pero no tanto para una especificación de idioma.fuente
if (x != null) x.foo(); x.bar();
, tenemos dos pruebas; laif
declaración es evidencia de la proposición "el autor cree que x podría ser nulo antes del llamado a engañar" y la siguiente declaración es evidencia de "el autor cree que x no es nulo antes del llamado a prohibir", y esta contradicción lleva a la conclusión de que hay un error. El error es el error relativamente benigno de una comprobación nula innecesaria o el error potencialmente fallido. Qué error es el error real no está claro, pero está claro que hay uno.Ha descubierto evidencia de que el algoritmo de flujo de programa que produce esta advertencia es relativamente poco sofisticado cuando se trata de rastrear los significados codificados en variables locales.
No tengo conocimiento específico de la implementación del verificador de flujo, pero después de haber trabajado en implementaciones de código similar en el pasado, puedo hacer algunas conjeturas. Es probable que el verificador de flujo deduzca dos cosas en el caso de falso positivo: (1)
_test
podría ser nulo, porque si no pudiera, no tendría la comparación en primer lugar, y (2)isNull
podría ser verdadero o falso, porque si no pudiera, no lo tendría en unif
. Pero la conexión quereturn _test;
solo se ejecuta si_test
no es nula, esa conexión no se está haciendo.Este es un problema sorprendentemente complicado, y debe esperar que al compilador le tome un tiempo lograr la sofisticación de las herramientas que han tenido varios años de trabajo por parte de expertos. El verificador de flujo Coverity, por ejemplo, no tendría ningún problema en deducir que ninguna de sus dos variaciones tuvo un rendimiento nulo, pero el verificador de flujo Coverity cuesta mucho dinero para los clientes corporativos.
Además, los verificadores de Coverity están diseñados para ejecutarse en grandes bases de código durante la noche ; El análisis del compilador de C # debe ejecutarse entre las pulsaciones de teclas en el editor , lo que cambia significativamente los tipos de análisis en profundidad que puede realizar razonablemente.
fuente
bool b = x != null
vsbool b = x is { }
(con ¡ninguna asignación realmente utilizada!) muestra que incluso los patrones reconocidos para las comprobaciones nulas son cuestionables. Para no menospreciar el trabajo indudablemente duro del equipo para hacer que esto funcione principalmente como debería para las bases de código reales en uso, parece que el análisis es pragmático capital P.Todas las otras respuestas son bastante correctas.
En caso de que alguien tenga curiosidad, traté de explicar la lógica del compilador de la manera más explícita posible en https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947
La única parte que no se menciona es cómo decidimos si un cheque nulo debe considerarse "puro", en el sentido de que si lo hace, deberíamos considerar seriamente si nulo es una posibilidad. Hay una gran cantidad de comprobaciones nulas "incidentales" en C #, donde se realiza una prueba de nulidad como parte de hacer otra cosa, por lo que decidimos que queríamos reducir el conjunto de comprobaciones a las que estábamos seguros de que la gente hacía deliberadamente. La heurística que se nos ocurrió fue "contiene la palabra nulo", así que es por eso
x != null
yx is object
produce resultados diferentes.fuente