web-dev-qa-db-ja.com

null参照が可能ではないように思われるときに、なぜ参照解除null参照の警告が表示されるのですか?

HNQで この質問 を読んだ後、 C#8のNullable参照型 について読み、いくつかの実験を行いました。

誰かが「コンパイラのバグを見つけた!」と言ったとき、私は10回のうち9回、またはもっと頻繁にそれを知っています。これは実際には設計によるものであり、彼ら自身の誤解です。そして、この機能の調査を今日から始めたので、明らかにあまりよく理解していません。これが邪魔にならないように、このコードを見てみましょう:

#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
    }
}

上記にリンクしたドキュメントを読んだ後、s == null行が警告を表示することを期待します。すべてのsは明らかにnullにできないため、これをnullと比較しても意味がありません。

代わりに、next行で警告が表示されます。警告では、sはnull参照である可能性がありますが、人間にとってはそうではないことは明らかです。

さらに、snullを比較しない場合、警告はnotと表示されます。

私はいくつかグーグルを実行し、 GitHubの問題 をヒットしましたが、それは完全に別の何かについてであることが判明しましたが、その過程で、この動作についてさらに洞察を与える寄稿者と会話しました(例 "NULLチェックは、変数のNULL可能性に関する以前の推論をリセットするようコンパイラーに指示するための有用な方法です。")しかし、それでもまだ主な質問には答えられませんでした。

新しいGitHubの問題を作成し、プロジェクトの非常に忙しいコントリビューターの時間を費やす可能性があるのではなく、これをコミュニティに公開します。

何が起こっているのか、その理由を教えてください。特に、s == null行で警告が生成されないのはなぜですか。また、ここでnull参照が可能ではないように見えるのに、なぜCS8602があるのですか?リンクされたGitHubスレッドが示唆しているように、null可能性の推論が完全ではない場合、どのようにして失敗するのでしょうか?その例は何でしょうか?

9
Andrew Savinykh

これは事実上、@ stuartdがリンクした回答の複製であるため、ここではこれ以上詳しく説明しません。しかし、問題の根本は、これが言語のバグでもコンパイラのバグでもないということですが、実装されたとおりの意図された動作です。変数のnull状態を追跡します。最初に変数を宣言するとき、その状態はNotNullです。これは、nullではない値で明示的に初期化するためです。ただし、そのNotNullがどこから来たかは追跡しません。たとえば、これは実質的に同等のコードです。

#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
    }
}

どちらの場合も、sに対してnullを明示的にテストします。 Madsがこの質問で回答したように、これをフロー分析への入力として使用します: https://stackoverflow.com/a/59328672/2672518 。その答えでは、結果として、返品時に警告が表示されます。この場合の答えは、nullの可能性がある参照を逆参照したという警告が表示されることです。

nullと比較するのに愚かだったからといって、null可能にはなりません。

うん、それは実際にはありません。 コンパイラーに。人間として、このコードを見て、明らかにnull参照例外をスローできないことを理解できます。しかし、ヌル可能フロー分析がコンパイラーに実装される方法は、それができません。値がどこから来たかに基づいて追加の状態を追加するこの分析のいくつかの改善点について説明しましたが、これにより、実装が大幅に複雑になるので、大きな利益は得られなかったと判断しました。これは、ユーザーがnewまたは定数値で変数を初期化し、それからnullをとにかくチェックするような場合に役立ちます。

5
333fred

Null可能性の推論が完全なものではない場合、[..]どのようにして失敗するのでしょうか?

C#8のnull可能な参照が利用可能になり次第、喜んで採用しました。私はReSharperの[NotNull](など)表記を使用していたので、2つの間にいくつかの違いに気づきました。

C#コンパイラはだまされる可能性がありますが、[側のエラー]の傾向があります(通常、常にではありません)。

将来の訪問者のためのリファレンスとして、これらは私がコンパイラーについてかなり混乱しているのを見たシナリオです(私はこれらのケースはすべて設計によると想定しています):

  • Null forniving null。逆参照警告を回避するためによく使用されますが、オブジェクトをnullにできないようにします。足を2つに留めておきたいようです。
    string s = null!; //No warning

  • 表面分析。 ReSharperとは対照的に(これは コード注釈 を使用して行われます)、C#コンパイラはnull可能な参照を処理するための属性の全範囲をまだサポートしていません。
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

ただし、警告を取り除くために、いくつかの構文を使用してnullが可能かどうかを確認できます。

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

または(まだかなりクールな)属性(それらすべてを検索 here ):

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

  • 影響を受けやすいコード分析。これがあなたが明らかにしたものです。コンパイラーは、機能するために仮定を立てる必要があり、(少なくとも人間にとって)直感に反するように見える場合があります。
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • ジェネリックに関する問題。質問 ここ とよく説明 ここ (以前と同じ記事、「Tの問題?」)ジェネリックは、参照と値の両方を満足させる必要があるため、複雑です。主な違いは、string?は単なる文字列ですが、int?Nullable<int>になり、コンパイラーにそれらを大幅に異なる方法で処理させることです。また、ここでは、コンパイラーが安全なパスを選択しているため、期待しているものを指定する必要があります。
    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.
    }

制約を与えて解決:

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

ただし、制約を使用せずに「?」を削除した場合Dataからは、 'default'キーワードを使用してnull値を入れることができます。

    [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.

安全ではないコードを書くことができるので、最後の方は私にとってトリッキーなようです。

これが誰かを助けることを願っています。

0
Alvin Sartor