Je peux faire un raisonnable devinez de ce qui se passe ici, mais c'est un peu compliqué :) Il s'agit de la l'état nul et le suivi nul décrits dans le projet de spécification . Fondamentalement, au moment où nous voulons retourner, le compilateur nous avertira si l'état de l'expression est "peut-être nul" au lieu de "non nul".
Cette réponse se présente sous une forme plus narrative que le simple "voici les conclusions"... J'espère qu'elle sera plus utile de cette façon.
Je vais simplifier légèrement l'exemple en me débarrassant des champs, et considérer 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 de pouvoir faire référence à des exemples spécifiques sans ambiguïté. Cela 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 retourner text
- donc c'est l'état nul de text
c'est important.
Retour inconditionnel
Tout d'abord, essayons de le renvoyer directement :
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
Jusqu'ici, c'est simple. L'état nullable du paramètre au début de la méthode est "peut-être null" s'il est de type string?
et "not null" s'il est de type string
.
Retour conditionnel simple
Maintenant, vérifions la présence de null dans le if
la condition de l'énoncé lui-même. (J'utiliserais l'opérateur conditionnel, qui, je pense, 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
}
}
Super, donc on dirait que dans un if
où la condition elle-même vérifie la nullité, l'état de la variable dans chaque branche de l'instruction if
peut être différente : dans le else
l'état est "non nul" dans les deux morceaux de code. Donc, en particulier, dans M3, l'état passe de "peut-être nul" à "non nul".
Retour conditionnel avec une variable locale
Essayons maintenant de faire passer cette condition dans 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 sites M5 et M6 émettent des avertissements. Ainsi, non seulement nous n'obtenons pas l'effet positif du changement d'état de "peut-être nul" à "non nul" dans M5 (comme c'était le cas dans M3)... mais nous obtenons l'effet positif de la modification de l'état. en face de dans M6, où l'état passe de "non nul" à "peut-être nul". Cela m'a vraiment surpris.
On dirait qu'on l'a appris :
- La logique autour de "comment une variable locale a été calculée" n'est pas utilisée pour propager des informations d'état. Nous y reviendrons plus tard.
- L'introduction d'une comparaison de nullité peut avertir le compilateur que quelque chose qu'il pensait ne pas être null pourrait l'être 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 semble être équivalent à M2 - les deux ont un paramètre not-null qu'ils retournent inconditionnellement - mais l'introduction d'une comparaison avec null change l'état de "not null" à "maybe null". Nous pouvons obtenir une preuve supplémentaire 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 comment le return
La déclaration n'a pas d'avertissement maintenant : l'État après exécution de text.Length
est "non nulle" (car si nous exécutons cette expression avec succès, elle ne peut pas être nulle). Ainsi, le text
commence par être "non nul" en raison de son type, devient "peut-être nul" en raison de la comparaison avec le paramètre nul, puis redevient "non nul" après l'application de la méthode de comparaison. text2.Length
.
Quelles comparaisons affectent l'état ?
Donc c'est une comparaison de text is null
... quel est l'effet de comparaisons similaires ? Voici quatre autres méthodes, qui commencent toutes par un paramètre de type chaîne de caractères non nul :
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
}
Donc, même si x is object
est désormais une alternative recommandée à x != null
ils n'ont pas le même effet : seule une comparaison est possible. avec null (avec l'un des is
, ==
ou !=
) fait passer l'état de "non nul" à "peut-être nul".
Pourquoi l'élévation de la condition a-t-elle un effet ?
Pour en revenir à notre premier point, 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. L'intégration de ce type de logique dans le compilateur et les spécifications représente beaucoup de travail, pour un bénéfice relativement faible. Voici un autre exemple qui n'a rien à voir avec la nullité et où l'inlining 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 sachez que alwaysTrue
sera toujours vrai, il ne satisfait pas aux exigences de la spécification qui font que le code après la balise if
inaccessible, ce qui est ce dont nous avons besoin.
Voici un autre exemple, autour de 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 savoir que le code entrera exactement une de ces if
Les corps de la déclaration, il n'y a rien dans la spécification pour travailler cela. Les outils d'analyse statique peuvent très bien être capables de le faire, mais essayer d'intégrer cela dans la spécification du langage serait une mauvaise idée, IMO - 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 langage.
1 votes
Debug.Assert n'est pas pertinent car il s'agit d'un contrôle au moment de l'exécution, alors que l'avertissement du compilateur est un contrôle au moment de la compilation. Le compilateur n'a pas accès au comportement du temps d'exécution.
0 votes
Je suppose que c'est la même chose que si vous faites
if(thisBool == true) { var x = 1; } else { var x = 0; } return x;
Ceci ne peut pas non plus être compilé, pour la raison que x n'est pas déclaré, même s'il existe à 100% à ce stade. Pour votre code, le compilateur ne peut tout simplement pas réaliser, que _test ne peut pas être null. Vous pourriez le mettre dans unif(_test != null)
6 votes
The Debug.Assert is irrelevant because that is a runtime check
- Il est pertinent car si vous commentez cette ligne, l'avertissement disparaît.1 votes
A mon avis, il s'agit d'un bug dans .NET 4.8.... un champ en lecture seule qui a déjà été initialisé ne sera jamais nul... Donc l'avertissement ne fonctionne pas correctement
0 votes
J'ai simplifié le code dans la question pour éviter les détails superflus. Cela ressemble un peu à une sorte de bug d'analyse.
0 votes
Il s'agit probablement d'un bogue dans Roslyn. Paging @jaredpar pour commentaire.
0 votes
Si vous remplacez
Debug.Assert
avecbool b = _test != null
(ou toute autre expression non triviale impliquant_test
), .NET Core donnera le même avertissement. Spéculation : la raison pour laquelle il se manifeste avecDebug.Assert
dans net48 mais pas dans Core est parce que dans Core,Debug.Assert
a unDoesNotReturnIf
qui permet au compilateur d'incorporer l'assert comme une vérification appropriée (mais notez que vous obtiendrez toujours l'avertissement avec l'annotationbool b = test != null; Debug.Assert(1 == 2)
donc il importe spécifiquement que la condition implique la variable). Il se peut qu'il y ait plus d'un bug en jeu ici.0 votes
@JeroenMostert Oui, j'ai découvert cela aussi - j'ai mis à jour la question pour supprimer l'information trompeuse.
0 votes
Je ne dirais pas que c'est trompeur, parce que c'est assez intéressant qu'avec
Debug.Assert
.NET Core se comporte comme prévu - cela semble être un comportement souhaitable à conserver dans tous les cas, même s'il s'avère que le comportement actuel est erroné. (Je dis "même si" parce que l'analyse est toujours un best effort, donc ne pas vérifier correctement la nullité ne peut pas toujours être appelé directement un bug même si cela semble vraiment évident et simple pour un cas particulier).0 votes
Dans .NET Framework 4.6.1 je ne vois pas cet avertissement
0 votes
@JeroenMostert L'analyse est censée se tromper en autorisant des choses qui pourraient être nulles, donc il ne devrait vraiment pas y avoir d'avertissement !
0 votes
Voici un peu plus de plaisir pour vous : avec
bool b = _test is null
l'avertissement demeure, mais avecbool b = _test is { }
il disparaît. Il y a encore moins de justification pour cela qu'avec==
commeis
ne souffre d'aucune complication possible avec les surcharges. Et bien sûr, le simple fait de tester la valeur sans agir sur elle ne devrait pas affecter l'analyse de nullité du tout.1 votes
@Polyfun : Le compilateur peut potentiellement savoir (via les attributs) que
Debug.Assert
lèvera une exception si le test échoue.2 votes
J'ai ajouté beaucoup de cas différents ici, et il y a des résultats vraiment intéressants. J'écrirai une réponse plus tard - j'ai du travail pour l'instant.
1 votes
" J'ai ajouté beaucoup de cas différents ici. ", ça va être le prochain RÉIMPLÉMENTATION DE LINQ serie. En ajoutant quelques tests, on obtient plus de 30 articles de blog.
1 votes
@xdtTransform : Ha ! Je ne pense pas avoir autant de temps, mais c'est une bonne idée :)
0 votes
Concernant
Debug.Assert
: Je ne sais pas ce que fait le vérificateur C#. Le vérificateur de flux Coverity a pour politique de traiter la condition affirmée comme un invariant ; rappelez-vous, le but d'une assertion est de documenter les invariants qui sont connus par le développeur mais pas par le compilateur ; si l'assertion est fausse, cela sera détecté par les tests.2 votes
@EricLippert :
Debug.Assert
a maintenant une annotation ( src ) deDoesNotReturnIf(false)
pour le paramètre de condition.1 votes
Par nul autre que le polyvalent Stephen Toub. Il ne cesse de m'impressionner.