Je peux faire une estimation raisonnable de ce qui se passe ici, mais c'est un peu compliqué :) Cela implique l' état nul et le suivi nul décrits dans le projet de spécification . Fondamentalement, au point où nous voulons revenir, le compilateur avertira si l'état de l'expression est "peut-être nul" au lieu de "non nul".
Cette réponse est plus ou moins narrative plutôt que "voici les conclusions" ... J'espère que c'est plus utile de cette façon.
Je vais simplifier légèrement l'exemple en supprimant les champs et envisager une méthode avec l'une de ces deux signatures:
public static string M(string? text)
public static string M(string text)
Dans les implémentations ci-dessous, j'ai donné à chaque méthode un numéro différent afin que je puisse faire référence à des exemples spécifiques sans ambiguïté. Il permet également à toutes les implémentations d'être présentes dans le même programme.
Dans chacun des cas décrits ci-dessous, nous ferons diverses choses, mais finirons par essayer de revenir text
- c'est donc l'état nul text
qui est important.
Retour inconditionnel
Tout d'abord, essayons simplement de le retourner directement:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
Jusqu'à présent, si simple. L'état nullable du paramètre au début de la méthode est "peut-être nul" s'il est de type string?
et "non nul" s'il est de type string
.
Retour conditionnel simple
Vérifions maintenant la valeur null dans la if
condition d'instruction elle-même. (J'utiliserais l'opérateur conditionnel, qui, je crois, aura le même effet, mais je voulais rester plus fidèle à la question.)
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
}
}
Génial, il ressemble donc à une if
instruction où la condition elle-même vérifie la nullité, l'état de la variable dans chaque branche de l' if
instruction peut être différent: dans le else
bloc, l'état n'est "pas nul" dans les deux morceaux de code. Ainsi, en particulier, dans M3, l'état passe de "peut-être nul" à "non nul".
Retour conditionnel avec une variable locale
Essayons maintenant de hisser cette condition à une variable locale:
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
}
}
Les deux avertissements d'émission M5 et M6. Donc non seulement nous n'obtenons pas l'effet positif du changement d'état de "peut-être nul" à "non nul" dans M5 (comme nous l'avons fait dans M3) ... nous obtenons l' effet inverse dans M6, où l'état va de " pas null "à" peut-être null ". Cela m'a vraiment surpris.
Il semble donc que nous ayons appris que:
- La logique autour de la façon dont une variable locale a été calculée n'est pas utilisée pour propager les informations d'état. Plus sur cela plus tard.
- L'introduction d'une comparaison nulle peut avertir le compilateur que quelque chose qu'il pensait précédemment non nul peut être nul après tout.
Retour inconditionnel après une comparaison ignorée
Examinons le deuxième de ces points, en introduisant une comparaison avant un retour inconditionnel. (Nous ignorons donc complètement le résultat de la comparaison.):
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
}
Notez comment M8 a l'impression qu'il devrait être équivalent à M2 - les deux ont un paramètre non nul qu'ils renvoient sans condition - mais l'introduction d'une comparaison avec null change l'état de "non nul" à "peut-être nul". Nous pouvons obtenir des preuves supplémentaires de cela en essayant de déréférencer text
avant la condition:
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
}
Notez que l' return
instruction n'a pas d'avertissement maintenant: l'état après l' exécution text.Length
n'est "pas nul" (car si nous exécutons cette expression avec succès, elle ne peut pas être nulle). Ainsi, le text
paramètre commence comme "non nul" en raison de son type, devient "peut-être nul" en raison de la comparaison nulle, puis redevient "non nul" après text2.Length
.
Quelles comparaisons affectent l'état?
Voilà donc une comparaison de text is null
... quel effet ont des comparaisons similaires? Voici quatre autres méthodes, toutes commençant par un paramètre de chaîne non nullable:
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
}
Ainsi, même si x is object
c'est maintenant une alternative recommandée à x != null
, ils n'ont pas le même effet: seule une comparaison avec null (avec n'importe lequel des is
, ==
ou !=
) change l'état de "non nul" à "peut-être nul".
Pourquoi le levage de la condition a-t-il un effet?
Pour en revenir à notre premier point précédent, pourquoi M5 et M6 ne tiennent-ils pas compte de la condition qui a conduit à la variable locale? Cela ne me surprend pas autant que cela semble surprendre les autres. Construire ce type de logique dans le compilateur et la spécification demande beaucoup de travail et relativement peu d'avantages. Voici un autre exemple qui n'a rien à voir avec la nullité où l'inclusion de quelque chose a un effet:
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
}
Même si nous savons que cela alwaysTrue
sera toujours vrai, cela ne satisfait pas aux exigences de la spécification qui rendent le code après l' if
instruction inaccessible, ce dont nous avons besoin.
Voici un autre exemple, concernant l'affectation définitive:
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);
}
Même si nous savons que le code entrera exactement dans l'un de ces if
corps de déclaration, il n'y a rien dans la spécification pour le résoudre. Les outils d'analyse statique pourraient bien être en mesure de le faire, mais essayer de mettre cela dans la spécification du langage serait une mauvaise idée, OMI - c'est bien pour les outils d'analyse statique d'avoir toutes sortes d'heuristiques qui peuvent évoluer dans le temps, mais pas tellement pour une spécification de langue.