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:
public static string M(string? text)
public static string M(string text)
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:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
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 tipo string
.
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).
public static string M3(string? text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
public static string M4(string text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
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 la if
declaración puede ser diferente: dentro del else
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:
public static string M5(string? text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
public static string M6(string text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
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:
- La lógica en torno a "cómo se calculó una variable local" no se usa para propagar información de estado. Más sobre eso más tarde.
- Introducir una comparación nula puede advertir al compilador que algo que antes pensaba que no era nulo podría ser nulo después de todo.
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):
public static string M7(string? text)
{
bool ignored = text is null;
return text; // Warning
}
public static string M8(string text)
{
bool ignored = text is null;
return text; // Warning
}
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:
public static string M9(string text)
{
int length1 = text.Length; // No warning
bool ignored = text is null;
int length2 = text.Length; // Warning
return text; // No warning
}
Observe cómo la return
declaración no tiene una advertencia ahora: el estado después de la ejecución text.Length
es "no nulo" (porque si ejecutamos esa expresión con éxito, no podría ser nula). Por lo tanto, el text
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" nuevamente text2.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:
public static string M10(string text)
{
bool ignored = text == null;
return text; // Warning
}
public static string M11(string text)
{
bool ignored = text is object;
return text; // No warning
}
public static string M12(string text)
{
bool ignored = text is { };
return text; // No warning
}
public static string M13(string text)
{
bool ignored = text != null;
return text; // Warning
}
Entonces, aunque x is object
ahora es una alternativa recomendada x != null
, no tienen el mismo efecto: solo una comparación con nulo (con cualquiera de is
, ==
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:
public static int X1()
{
if (true)
{
return 1;
}
}
public static int X2()
{
bool alwaysTrue = true;
if (alwaysTrue)
{
return 1;
}
// Error: not all code paths return a value
}
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 la if
declaración inalcanzable, que es lo que necesitamos.
Aquí hay otro ejemplo, alrededor de la asignación definida:
public static void X3()
{
string x;
bool condition = DateTime.UtcNow.Year == 2020;
if (condition)
{
x = "It's 2020.";
}
if (!condition)
{
x = "It's not 2020.";
}
// Error: x is not definitely assigned
Console.WriteLine(x);
}
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.