web-dev-qa-db-fra.com

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?

Après avoir lu cette question sur HNQ, j'ai continué à lire 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, clairement Je ne la comprends pas très bien. 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 le s == null ligne pour me donner un avertissement - après tout, s est clairement non nul, donc le comparer à null n'a pas de sens.

Au lieu de cela, je reçois un avertissement sur la ligne - suivant, et l'avertissement dit que s est possible une référence nulle, même si, pour un humain, il est évident que c'est ne pas.

De plus, l'avertissement n'est pas affiché si nous ne comparons pas s à null.

J'ai fait une recherche sur Google et j'ai frappé n problème GitHub , ce qui s'est avéré être quelque chose d'autre, mais dans le processus, j'ai eu une conversation avec un contributeur qui a donné un peu plus de perspicacité dans ce comportement (par exemple = "Les vérifications nulles sont souvent un moyen utile de dire au compilateur de réinitialiser son inférence antérieure 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 potentiellement de prendre le temps des contributeurs du projet incroyablement occupés, je le fais savoir à la communauté.

Pourriez-vous m'expliquer ce qui se passe et pourquoi? En particulier, pourquoi aucun avertissement n'est généré sur le s == null line, et pourquoi avons-nous CS8602 quand il ne semble pas qu'une référence null 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?

9
Andrew Savinykh

Il s'agit en fait d'un doublon de la réponse liée à @stuartd, 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 s pour 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 nullable, simplement parce que nous étions assez idiots pour le comparer avec null.

Oui, c'est le cas. Pour le 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 un new ou une valeur constante, puis le vérifie pour null de toute façon.

5
333fred

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

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 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 . Demandé ici et très bien expliqué 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 force le compilateur à les gérer de manières sensiblement différentes. Ici aussi, le compilateur choisit le chemin sécurisé, vous obligeant à 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.

0
Alvin Sartor