Considérez le code suivant:
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 = "";
}
}
Quand je construis cela, la ligne marquée !!!
donne un avertissement du compilateur: warning CS8603: Possible null reference return.
.
Je trouve cela un peu déroutant, étant donné qu'il _test
est en lecture seule et initialisé à non nul.
Si je change le code comme suit, l'avertissement disparaît:
public string Test()
{
// bool isNull = _test == null;
if (_test == null)
return "";
else
return _test;
}
Quelqu'un peut-il expliquer ce comportement?
c#
nullable-reference-types
Matthew Watson
la source
la source
The Debug.Assert is irrelevant because that is a runtime check
- Il est pertinent , car si vous commentez que sur la ligne, l'avertissement disparaît.Debug.Assert
une exception si le test échoue.Debug.Assert
maintenant une annotation ( src )DoesNotReturnIf(false)
pour le paramètre de condition.Réponses:
L'analyse de flux annulable suit l' état nul des variables, mais elle ne suit pas les autres états, tels que la valeur d'une
bool
variable (commeisNull
ci-dessus), et elle ne suit pas la relation entre l'état des variables distinctes (par exempleisNull
et_test
).Un moteur d'analyse statique réel ferait probablement ces choses, mais serait également "heuristique" ou "arbitraire" dans une certaine mesure: vous ne pourriez pas nécessairement dire les règles qu'il suivait, et ces règles pourraient même changer avec le temps.
Ce n'est pas quelque chose que nous pouvons faire directement dans le compilateur C #. Les règles pour les avertissements annulables sont assez sophistiquées (comme le montre l'analyse de Jon!), Mais ce sont des règles et peuvent être motivées.
Au fur et à mesure que nous déployons la fonctionnalité, il semble que nous ayons principalement atteint le bon équilibre, mais il y a quelques endroits qui semblent aussi maladroits, et nous reviendrons sur ceux de C # 9.0.
la source
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:
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 nultext
qui est important.Retour inconditionnel
Tout d'abord, essayons simplement de le retourner directement:
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 typestring
.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.)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 leelse
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:
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:
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.):
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:Notez que l'
return
instruction n'a pas d'avertissement maintenant: l'état après l' exécutiontext.Length
n'est "pas nul" (car si nous exécutons cette expression avec succès, elle ne peut pas être nulle). Ainsi, letext
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èstext2.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: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 desis
,==
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:
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:
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.la source
if (x != null) x.foo(); x.bar();
nous avons deux éléments de preuve; laif
déclaration est la preuve de la proposition "l'auteur pense que x pourrait être nul avant l'appel à foo" et la déclaration suivante est la preuve de "l'auteur pense que x n'est pas nul avant l'appel à la barre", et cette contradiction conduit à la conclusion qu'il y a un bug. Le bogue est soit le bogue relativement bénin d'une vérification nulle inutile, soit le bogue potentiellement en panne. Quel bug est le vrai bug n'est pas clair, mais il est clair qu'il y en a un.Vous avez découvert des preuves que l'algorithme de flux de programme qui produit cet avertissement est relativement peu sophistiqué lorsqu'il s'agit de suivre les significations codées dans les variables locales.
Je n'ai aucune connaissance spécifique de l'implémentation du vérificateur de flux, mais ayant travaillé sur des implémentations de code similaire dans le passé, je peux faire des suppositions éclairées. Le vérificateur de flux déduit probablement deux choses dans le cas des faux positifs: (1)
_test
pourrait être nul, car si ce n'était pas le cas, vous n'auriez pas la comparaison en premier lieu, et (2)isNull
pourrait être vrai ou faux - parce que s'il ne le pouvait pas, vous ne l'auriez pas dans unif
. Mais la connexion quireturn _test;
s'exécute uniquement_test
n'est pas nulle, cette connexion n'est pas établie.Il s'agit d'un problème étonnamment délicat, et vous devez vous attendre à ce que le compilateur prenne un certain temps pour atteindre la sophistication d'outils qui ont nécessité plusieurs années de travail par des experts. Le vérificateur de flux Coverity, par exemple, n'aurait aucun problème à déduire qu'aucune de vos deux variantes n'a eu un retour nul, mais le vérificateur de flux Coverity coûte beaucoup d'argent pour les entreprises clientes.
En outre, les vérificateurs de couverture sont conçus pour fonctionner sur de grandes bases de code pendant la nuit ; l'analyse du compilateur C # doit s'exécuter entre les frappes de touches dans l'éditeur , ce qui modifie considérablement les types d'analyses approfondies que vous pouvez raisonnablement effectuer.
la source
bool b = x != null
VSbool b = x is { }
(avec aucune des affectations réellement utilisées!) montre que même les modèles reconnus pour les contrôles nuls sont discutables. Pour ne pas dénigrer le travail sans aucun doute dur de l'équipe pour faire ce travail principalement comme il se doit pour des bases de code réelles et en cours d'utilisation - il semble que l'analyse soit pragmatique en capital-P.Toutes les autres réponses sont à peu près exactement correctes.
Au cas où quelqu'un serait curieux, j'ai essayé d'énoncer la logique du compilateur aussi explicitement que possible dans https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947
Le seul élément qui n'est pas mentionné est la façon dont nous décidons si un chèque nul doit être considéré comme «pur», dans le sens où si vous le faites, nous devrions sérieusement considérer si la nullité est une possibilité. Il y a beaucoup de vérifications nulles "accessoires" en C #, où vous testez la nullité dans le cadre de quelque chose d'autre, nous avons donc décidé de restreindre l'ensemble des vérifications à celles que nous étions sûrs que les gens faisaient délibérément. L'heuristique que nous avons trouvée était "contient le mot null", c'est pourquoi
x != null
etx is object
produire des résultats différents.la source