なぜこのコードは「可能なnull参照の戻り値」コンパイラ警告を出すのですか


70

次のコードを検討してください。

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 = "";
    }
}

これをビルドすると、でマークされた行で!!!コンパイラ警告が表示されますwarning CS8603: Possible null reference return.

_test読み取り専用であり、null以外に初期化されているため、これは少し混乱します。

コードを次のように変更すると、警告が消えます。

        public string Test()
        {
            // bool isNull = _test == null;

            if (_test == null)
                return "";
            else
                return _test;
        }

誰かがこの動作を説明できますか?


1
Debug.Assertはランタイムチェックであるため無関係ですが、コンパイラの警告はコンパイル時のチェックです。コンパイラーはランタイム動作にアクセスできません。
Polyfun 2019

5
The Debug.Assert is irrelevant because that is a runtime check-それはであるあなたがその行をコメントアウトした場合、警告が消えるので、関連します。
マシューワトソン

1
@Polyfun:コンパイラはDebug.Assert、テストが失敗した場合に例外をスローする(属性を介して)可能性があることを知ることができます。
Jon Skeet

2
ここではさまざまなケースを追加してきましたが、興味深い結果がいくつかあります。後で答えを書いていきます-今のところやるべきこと。
Jon Skeet、

2
@EricLippert:条件パラメーターのDebug.Assert注釈(src)が追加さDoesNotReturnIf(false)れました。
Jon Skeet

回答:


38

nullableフロー分析は、変数のnull状態を追跡しますが、boolisNull上記の)変数の値などの他の状態は追跡しません。isNullまた、個別の変数の状態間の関係(およびなど_test)は追跡しません。

実際の静的分析エンジンは、おそらくこれらのことを行いますが、ある程度は「ヒューリスティック」または「任意」になります。従うルールを必ずしも伝えることができず、それらのルールは時間とともに変化する可能性さえあります。

これは、C#コンパイラで直接実行できることではありません。null可能警告のルールは(Jonの分析が示すように)非常に洗練されていますが、それらはルールであり、推論することができます。

この機能を展開するにつれ、ほとんどが適切なバランスに達したように感じられますが、厄介なところがいくつかあります。C#9.0でそれらを再検討します。


3
あなたは格子理論を仕様に入れたいと思っています。格子理論は素晴らしく、まったく混乱しません!やれ!:)
Eric Lippert

7
C#のプログラムマネージャーが応答するとき、あなたの質問は正当です。
Sam Rueby

1
@TanveerBadar:格子理論は、半順序を持つ値のセットの分析に関するものです。タイプは良い例です。タイプXの値がタイプYの変数に割り当て可能である場合、それはYがXを保持するのに十分な大きさであり、それがラティスを形成するのに十分であることを意味します。格子理論の観点からの仕様。タイプの割り当て可能性以外のアナライザーにとって興味深い非常に多くのトピックもラティスの観点から表現できるため、これは静的分析に関連しています。
Eric Lippert

1
@TanveerBadar:lara.epfl.ch/w/_media/sav08: schwartzbach.pdfには、静的分析エンジンが格子理論を使用する方法の優れた導入例がいくつかあります。
Eric Lippert

1
@EricLippert Awesomeはあなたを説明し始めません。そのリンクは、私の必読リストにすぐ入ります。
Tanveer Badar

56

私はここで何が起こっているのかについて合理的な推測をすることができますが、それはすべて少し複雑です:)これには、ドラフト仕様で説明されているnull状態とnullトラッキングが含まれます。基本的に、戻りたいポイントで、式の状態が「nullでない」ではなく「nullかもしれない」場合、コンパイラーは警告を出します。

この答えは、「ここに結論があります」というよりはむしろいくぶん物語的な形になっています...私はそれがそのようにより有用であることを望みます。

フィールドを取り除くことで例を少し単純化し、次の2つのシグネチャのいずれかを持つメソッドを検討します。

public static string M(string? text)
public static string M(string text)

以下の実装では、各メソッドに異なる番号を付けているため、特定の例を明確に参照できます。また、すべての実装を同じプログラムに含めることができます。

以下で説明するそれぞれのケースでは、さまざまなことを行いますが、最終的には戻ろうとするtextため、それはnull状態であるtextことが重要です。

無条件返品

最初に、直接返してみましょう:

public static string M1(string? text) => text; // Warning
public static string M2(string text) => text;  // No warning

これまでのところ、とても簡単です。メソッドの開始時のパラメータのnull可能状態は、それがtypeの場合は「多分null」であり、typeのstring?場合は「not null」ですstring

単純な条件付きリターン

次に、ifステートメント条件自体の中でnullをチェックします。(私は条件演算子を使用しますが、これは同じ効果があると思いますが、質問に対してより忠実であり続けたいと思いました。)

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

すばらしいのでif、条件自体がnull かどうかをチェックするステートメント内のように見えます。ステートメントの各ブランチ内の変数の状態はif異なる場合があります。elseブロック内では、状態は両方のコードで「nullではない」です。したがって、特にM3では、状態が「nullかもしれない」から「nullでない」に変わります。

ローカル変数による条件付き戻り

次に、その条件をローカル変数に巻き上げてみましょう。

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

M5とM6の両方が警告を発行します。したがって、M5での「多分null」から「not null」への状態変化のプラスの効果が得られないだけでなく(M3で行ったように)、M6で反対の効果が得られます。nullではない」から「nullかもしれない」まで。本当にびっくりしました。

つまり、次のことを学んだようです。

  • 「ローカル変数の計算方法」に関するロジックは、状態情報の伝達には使用されません。詳細は後ほど。
  • null比較を導入すると、以前はnullではないと考えていたものが結局nullになる可能性があることをコンパイラに警告できます。

無視された比較の後の無条件の戻り

無条件に戻る前に比較を導入して、これらの箇条書きの2番目を見てみましょう。(したがって、比較の結果は完全に無視しています。):

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
}

M8がM2と同等であるように見えることに注意してください-どちらも無条件で返される非nullパラメータを持っています-しかし、nullとの比較を導入すると、状態が「非null」から「多分null」に変わります。text条件の前に逆参照を試みることにより、これのさらなる証拠を得ることができます。

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
}

returnステートメントに警告が表示されていないことに注意してください。実行の状態text.Lengthは「nullではない」です(その式を正常に実行した場合、nullにできないため)。そのため、textパラメーターは、そのタイプのために「nullではない」として始まり、null比較のために「nullかもしれない」になり、その後再び「nullでない」になりtext2.Lengthます。

どのような比較が状態に影響しますか?

それが比較ですtext is null...同様の比較はどのような効果をもたらしますか?次に、4つのメソッドを示します。すべてnull不可の文字列パラメーターで始まります。

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
}

したがって、x is objectがの代替として推奨されている場合でもx != null、同じ効果はありません。null の比較(is==またはのいずれかを使用!=)のみが、状態を「null以外」から「おそらくnull」に変更します。

なぜ状態を上げると効果があるのですか?

前の最初の箇条書きに戻って、M5とM6がローカル変数の原因となった条件を考慮しないのはなぜですか?これは、他の人を驚かせるように見えるほど私を驚かせません。この種のロジックをコンパイラと仕様に組み込むことは多くの作業であり、メリットはほとんどありません。何かをインライン化することで影響が出るnull可能性とは何の関係もない別の例を次に示します。

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
}

にもかかわらず、我々はそれが知っているalwaysTrue、常にtrueになります、それは後に、コードを作る仕様で要件満たしていないif私たちが必要とするものである到達不可能な文を、。

明確な割り当てに関する別の例を次に示します。

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);
}

コードがこれらのステートメント本体の1つに正確に入ることがわかっている場合でもif、仕様にはそれを解決するものは何もありません。静的分析ツールはそうすることができるかもしれませんが、言語仕様にそれを入れようとすることは悪い考えです、IMO-静的分析ツールがすべての種類のヒューリスティックを持っていることは問題ありません。言語仕様。


7
素晴らしい分析ジョン。コベリティチェッカーを研究したときに私が学んだ重要なことは、コードはその作者の信念の証拠であることです。コードの作成者がチェックが必要であると信じていることを私たちに知らせるはずのnullチェックを見つけたとき。チェッカーが実際に作成しているのは、バグが発生するという、たとえば無効性について一貫性のない信念が見られる場所であるため、著者の信念が一貫していないという証拠です
Eric Lippert

6
たとえば、if (x != null) x.foo(); x.bar();私たちが見た場合、2つの証拠があります。このifステートメントは、「著者はfooを呼び出す前にxがnullである可能性があると信じている」という命題の証拠であり、次のステートメントは「著者がxがbarを呼び出す前にnullでないと信じている」という証拠であり、この矛盾はバグがあるという結論。バグは、不要なnullチェックの比較的害のないバグか、クラッシュする可能性のあるバグのいずれかです。実際のバグであるバグは明確ではありませんが、バグがあることは明らかです。
Eric Lippert

1
比較的洗練されていないチェッカーがローカルの意味を追跡せず、「偽のパス」(人間があなたに告げることができる制御フローパスは不可能であると制御できない)が問題を正確にモデル化していないため正確に偽陽性を生成する傾向があるという問題著者の信念。それはトリッキーなビットです!
Eric Lippert

3
「is object」、「is {}」、「!= null」の間の不整合は、過去数週間内部的に議論してきた項目です。これらを純粋なnullチェックと見なす必要があるかどうかを判断するために、非常に近い将来にLDMでそれを取り上げる予定です(これにより、動作が一貫します)。
JaredPar

1
@ArnonAxelrodそれはそれがnullであること意味しないと言います。null可能な参照型はコンパイラのヒントにすぎないため、それでもnullである可能性があります。(例:M8(null!);またはC#7コードから呼び出すか、警告を無視します。)プラットフォームの他の部分のタイプセーフとは異なります。
Jon Skeet、

29

ローカル変数にエンコードされた意味の追跡に関しては、この警告を生成するプログラムフローアルゴリズムが比較的単純であるという証拠を発見しました。

私はフローチェッカーの実装について特定の知識はありませんが、過去に同様のコードの実装に取り​​組んだことがあるので、知識に基づいた推測を行うことができます。フローチェッカーは、偽陽性の場合に2つのことを推測している可能性_testがあります。(1)nullである可能性があります。それができない場合は、最初に比較ができないため、(2)isNulltrueまたはfalseである可能性があります。それができない場合は、に含まれませんif。しかし、return _test;唯一実行さ_testれる接続がnullでない場合、その接続は確立されていません。

これは驚くほどトリッキーな問題であり、専門家による何年にもわたる作業を行ってきたツールの高度化をコンパイラーが実現するには、しばらく時間がかかることを期待する必要があります。たとえば、Coverityフローチェッカーは、2つのバリエーションのどちらにもnullリターンがなかったと推定してもまったく問題ありませんが、Coverityフローチェッカーは、企業の顧客に多大な費用がかかります。

また、Coverityチェッカーは、大規模なコードベースで一晩実行するように設計されています。C#コンパイラーの分析は、エディターのキーストローク間で実行する必要があります。これにより、合理的に実行できる詳細な分析の種類が大幅に変わります。


「洗練されていない」は正しいです。条件付き問題などに出くわした場合、それは許されると思います。停止の問題はそのような問題では少し難しいことですが、bool b = x != nullvs bool b = x is { }(どちらの割り当ても実際には使用されません!)は、nullチェックの認識されたパターンでさえ疑わしいことを示しています。チームの間違いなく困難な作業を軽視して、この作業を実際の使用中のコードベースの場合とほぼ同じように行わないようにしないでください。分析は重要で実用的なもののようです。
Jeroen Mostert

@JeroenMostert:Jared Parは、Jon Skeetの回答に対するコメントで、Microsoftがその問題について社内で話し合っていると述べています。
ブライアン、

8

他のすべての答えはかなり正確です。

誰かが気になった場合のために、私はコンパイラのロジックを可能な限り明示的に説明しようとしました https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947で

言及されていないのは、nullチェックを「純粋」と見なすかどうかをどのように決定するかです。つまり、そうする場合、nullが可能かどうかを真剣に検討する必要があります。C#には「偶発的」なnullチェックが多数あり、他の何かを行う一環としてnullをテストするため、チェックのセットを、人々が意図的に行っていると確信しているチェックに絞り込むことにしました。私たちが思い付いたヒューリスティックは「nullという単語を含む」でした。そのためx != nullx is object異なる結果が生成されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.