Pourquoi recevons-nous un avertissement de référence nulle de déréférence possible, alors que la référence nulle ne semble pas être possible?

9

Après avoir lu cette question sur HNQ, j'ai continué à lire sur les types de référence Nullable en C # 8 et j'ai fait quelques expériences.

Je suis très conscient que 9 fois sur 10, voire plus souvent, quand quelqu'un dit "J'ai trouvé un bug de compilation!" c'est en fait par conception, et leur propre malentendu. Et depuis que j'ai commencé à étudier cette fonctionnalité seulement aujourd'hui, je ne l'ai clairement pas très bien comprise. Avec cela à l'écart, regardons ce code:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Après avoir lu la documentation que j'ai liée à ci-dessus, je m'attendrais à ce que la s == nullligne me donne un avertissement - après tout, il sest clairement non nul, donc la comparer à nulln'a pas de sens.

Au lieu de cela, je reçois un avertissement sur la ligne suivante , et l'avertissement dit que sc'est possible une référence nulle, même si, pour un humain, il est évident que ce n'est pas le cas.

De plus, l'avertissement ne s'affiche pas si nous ne comparons pas sà null.

J'ai fait quelques recherches sur Google et j'ai rencontré un problème avec GitHub , qui s'est avéré être quelque chose de complètement différent, mais au cours du processus, j'ai eu une conversation avec un contributeur qui a donné un aperçu de ce comportement (par exemple, "les vérifications nulles sont souvent un moyen utile de dire au compilateur de réinitialiser son inférence précédente sur la nullité d'une variable. " ). Cela m'a cependant laissé la question principale sans réponse.

Plutôt que de créer un nouveau problème GitHub, et de prendre potentiellement le temps des contributeurs du projet incroyablement occupés, je mets cela à la disposition de la communauté.

Pourriez-vous m'expliquer ce qui se passe et pourquoi? En particulier, pourquoi aucun avertissement n'est généré sur la s == nullligne, et pourquoi avons-nous CS8602quand il ne semble pas qu'une nullréférence soit possible ici? Si l'inférence de nullité n'est pas à l'épreuve des balles, comme le suggère le thread GitHub lié, comment peut-il mal tourner? Quels seraient quelques exemples de cela?

Andrew Savinykh
la source
Il semble que le compilateur lui-même définit un comportement qui, à ce stade, la variable "s" pourrait être nulle. Quoi qu'il en soit, si j'utilise des chaînes ou des objets, il devrait toujours y avoir une vérification avant d'appeler une fonction. "s? .Length" devrait faire l'affaire et l'avertissement lui-même devrait disparaître.
chg
1
@chg, il ne devrait pas être nécessaire ?car il sn'est pas nullable. Il ne devient pas annulable, simplement parce que nous étions assez stupides pour le comparer null.
Andrew Savinykh
Je suivais une question précédente (désolé, je ne la trouve pas) où il était posé que si vous ajoutez une vérification qu'une valeur est nulle, alors le compilateur prend cela comme un "indice" que la valeur pourrait être nulle, même si cela n'est manifestement pas le cas.
stuartd
@stuartd, yep, c'est ce qu'il semble que ce soit. Alors maintenant, la question est: pourquoi est-ce utile?
Andrew Savinykh
1
@chg, eh bien, c'est ce que je dis dans le corps de la question, n'est-ce pas?
Andrew Savinykh

Réponses:

7

C'est en fait un doublon de la réponse que @stuartd a liée, donc je ne vais pas entrer dans les détails super profonds ici. Mais la racine du problème est que ce n'est ni un bogue de langue ni un bogue de compilateur, mais c'est un comportement prévu exactement tel qu'implémenté. Nous suivons l'état nul d'une variable. Lorsque vous déclarez initialement la variable, cet état est NotNull car vous l'initialisez explicitement avec une valeur qui n'est pas nulle. Mais nous ne savons pas d'où vient ce NotNull. Ceci, par exemple, est en fait un code équivalent:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Dans les deux cas, vous testez explicitement spour null. Nous prenons cela comme entrée dans l'analyse de flux, tout comme Mads a répondu dans cette question: https://stackoverflow.com/a/59328672/2672518 . Dans cette réponse, le résultat est que vous obtenez un avertissement sur le retour. Dans ce cas, la réponse est que vous obtenez un avertissement que vous avez déréférencé une référence éventuellement nulle.

Il ne devient pas annulable, simplement parce que nous étions assez stupides pour le comparer null.

Oui, en fait. Au compilateur. En tant qu'humains, nous pouvons regarder ce code et évidemment comprendre qu'il ne peut pas lever d'exception de référence nulle. Mais la façon dont l'analyse de flux nullable est implémentée dans le compilateur ne le peut pas. Nous avons discuté d'une certaine quantité d'améliorations à cette analyse où nous ajoutons des états supplémentaires en fonction de la provenance de la valeur, mais nous avons décidé que cela ajoutait beaucoup de complexité à la mise en œuvre pour pas beaucoup de gain, car les seuls endroits où cela serait utile dans des cas comme celui-ci, où l'utilisateur initialise une variable avec une newou une valeur constante, puis la vérifie nullquand même.

333fred
la source
Je vous remercie. Cela traite principalement des similitudes avec l'autre question, je voudrais aborder davantage les différences. Par exemple, pourquoi s == nullne produit-il pas d'avertissement?
Andrew Savinykh
Je viens aussi de réaliser cela avec #nullable enable; string s = "";s = null;compile et fonctionne (il produit toujours un avertissement) quels sont les avantages de l'implémentation qui permet d'affecter null à une "référence non nullable" dans un contexte d'annotation null activé?
Andrew Savinykh
La réponse de Mad se concentre sur le fait que "[le compilateur] ne suit pas la relation entre l'état des variables distinctes" nous n'avons pas de variables distinctes dans cet exemple, j'ai donc du mal à appliquer le reste de la réponse de Mad à ce cas.
Andrew Savinykh
Cela va sans dire, mais souvenez-vous, je ne suis pas ici pour critiquer, mais pour apprendre. J'utilise C # depuis sa sortie en 2001. Même si je ne suis pas nouveau dans le langage, le comportement du compilateur m'a surpris. Le but de cette question est de comprendre pourquoi ce comportement est utile aux humains.
Andrew Savinykh
Il y a des raisons valables de vérifier s == null. Par exemple, vous êtes peut-être dans une méthode publique et vous souhaitez effectuer la validation des paramètres. Ou, peut-être que vous utilisez une bibliothèque qui n'a pas été correctement annotée, et jusqu'à ce qu'ils corrigent ce bogue, vous devez gérer null là où il n'a pas été déclaré. Dans l'un ou l'autre de ces cas, si nous mettions en garde, ce serait une mauvaise expérience. Quant à permettre l'affectation: les annotations de variables locales sont juste pour la lecture. Ils n'affectent pas du tout l'exécution. En fait, nous mettons tous ces avertissements dans un seul code d'erreur afin que vous puissiez les désactiver si vous souhaitez réduire le taux de désabonnement du code.
333fred
0

Si l'inférence de nullité n'est pas à l'épreuve des balles, [..] comment peut-elle mal tourner?

J'ai volontiers adopté les références annulables de C # 8 dès qu'elles étaient disponibles. Comme j'étais habitué à utiliser la notation [NotNull] (etc.) de ReSharper, j'ai remarqué quelques différences entre les deux.

Le compilateur C # peut être dupe, mais il a tendance à pécher par excès de prudence (généralement, pas toujours).

Comme référence pour les futurs visiteurs, voici les scénarios pour lesquels j'ai vu le compilateur être assez confus (je suppose que tous ces cas sont de par leur conception):

  • Null pardonnant null . Souvent utilisé pour éviter l'avertissement de déréférencement, mais en gardant l'objet non nullable. Il semble vouloir garder son pied dans deux chaussures.
    string s = null!; //No warning

  • Analyse de surface . Par opposition à ReSharper (qui le fait en utilisant l' annotation de code ), le compilateur C # ne prend toujours pas en charge une gamme complète d'attributs pour gérer les références nullables.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Il permet cependant d'utiliser une construction pour vérifier la nullité qui supprime également l'avertissement:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

ou (encore assez cool) des attributs (trouvez-les tous ici ):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Analyse de code sensible . C'est ce que vous avez mis en lumière. Le compilateur doit faire des hypothèses pour fonctionner et parfois elles peuvent sembler contre-intuitives (pour les humains, au moins).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Problèmes avec les génériques . Interrogé ici et expliqué très bien ici (même article que précédemment, paragraphe "Le problème avec T?"). Les génériques sont compliqués car ils doivent rendre les références et les valeurs heureuses. La principale différence est que tandis que string?n'est qu'une chaîne, int?devient un Nullable<int>et oblige le compilateur à les gérer de manière sensiblement différente. Ici aussi, le compilateur choisit le chemin d'accès sécurisé, vous forçant à spécifier ce que vous attendez:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Résolu donnant des contraintes:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

Mais si nous n'utilisons pas de contraintes et supprimons le "?" à partir de Data, nous pouvons toujours y mettre des valeurs nulles en utilisant le mot-clé 'default':

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

Le dernier me semble le plus délicat, car il permet d'écrire du code dangereux.

J'espère que cela aide quelqu'un.

Alvin Sartor
la source
Je suggère de lire les documents sur les attributs nullables : docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Ils résoudront certains de vos problèmes, en particulier avec la section d'analyse de surface et les génériques.
333fred
Merci pour le lien @ 333fred. Même s'il existe des attributs avec lesquels vous pouvez jouer, un attribut qui résout le problème que j'ai signalé (quelque chose qui me dit qu'il ThrowIfNull(s);m'assure qu'il sn'est pas nul), n'existe pas. L'article explique également comment gérer les génériques non nullables , alors que je montrais comment vous pouvez "tromper" le compilateur, ayant une valeur nulle mais aucun avertissement à ce sujet.
Alvin Sartor
En fait, l'attribut existe. J'ai déposé un bug sur les documents pour l'ajouter. Vous cherchez DoesNotReturnIf(bool).
333fred
@ 333fred en fait je cherche quelque chose de plus DoesNotReturnIfNull(nullable).
Alvin Sartor