web-dev-qa-db-ja.com

好奇心null盛なヌル結合演算子のカスタム暗黙的変換動作

注:これは Roslynで修正されたようです

this one への私の答えを書くときにこの質問が生じました。これは null-coalescing operator の結合性について述べています。

念のため、null合体演算子の考え方は、次の形式の式です。

x ?? y

最初にxを評価し、次に:

  • xの値がnullの場合、yが評価され、それが式の最終結果になります
  • xの値がNULLでない場合、ynot評価され、xの値は式、必要に応じてyのコンパイル時型への変換後

現在通常変換の必要はありません。または、nullを許可する型からnullを許可しない型に変換するだけです。通常、型は同じであるか、(たとえば)int?からintへ。ただし、can独自の暗黙的な変換演算子を作成できます。これらは必要に応じて使用されます。

x ?? yの単純なケースでは、奇妙な振る舞いを見たことはありません。ただし、(x ?? y) ?? zを使用すると、混乱する動作が発生します。

これは短いが完全なテストプログラムです-結果はコメントにあります:

using System;

public struct A
{
    public static implicit operator B(A input)
    {
        Console.WriteLine("A to B");
        return new B();
    }

    public static implicit operator C(A input)
    {
        Console.WriteLine("A to C");
        return new C();
    }
}

public struct B
{
    public static implicit operator C(B input)
    {
        Console.WriteLine("B to C");
        return new C();
    }
}

public struct C {}

class Test
{
    static void Main()
    {
        A? x = new A();
        B? y = new B();
        C? z = new C();
        C zNotNull = new C();

        Console.WriteLine("First case");
        // This prints
        // A to B
        // A to B
        // B to C
        C? first = (x ?? y) ?? z;

        Console.WriteLine("Second case");
        // This prints
        // A to B
        // B to C
        var tmp = x ?? y;
        C? second = tmp ?? z;

        Console.WriteLine("Third case");
        // This prints
        // A to B
        // B to C
        C? third = (x ?? y) ?? zNotNull;
    }
}

したがって、3つのカスタム値タイプ、AB、およびCがあり、AからB、AからC、およびBからCに変換されます。

私は2番目のケースと3番目のケースの両方を理解できます...しかし、なぜ最初のケースに余分なAからBへの変換がありますか?特に、私はreallyが最初のケースと2番目のケースが同じであることを期待していました-結局のところ、ローカル変数に式を抽出するだけです。

何が起こっているのか?私はC#コンパイラに関しては「バグ」を叫ぶことを非常にheしますが、何が起こっているのか迷っています...

編集:さて、ここで起こっていることの厄介な例があります、コンフィギュレーターの答えのおかげで、それがバグだと思うさらなる理由を与えてくれます。編集:サンプルは今では2つのヌル合体演算子も必要としません...

using System;

public struct A
{
    public static implicit operator int(A input)
    {
        Console.WriteLine("A to int");
        return 10;
    }
}

class Test
{
    static A? Foo()
    {
        Console.WriteLine("Foo() called");
        return new A();
    }

    static void Main()
    {
        int? y = 10;

        int? result = Foo() ?? y;
    }
}

これの出力は次のとおりです。

Foo() called
Foo() called
A to int

ここでFoo()が2回呼び出されるという事実は、私にとって非常に驚くべきことです。式がevaluatedである理由は2回わかりません。

526
Jon Skeet

この問題の分析に貢献したすべての人に感謝します。これは明らかにコンパイラのバグです。それは、合体演算子の左側にある2つのNULL可能型に関係するリフト変換がある場合にのみ発生するようです。

正確にどこがうまくいかないかはまだ特定していませんが、コンパイルの「nullable lowering」フェーズのある時点で-最初の分析の後、コード生成の前に-式を減らします

result = Foo() ?? y;

上記の例から道徳的な同等物まで:

A? temp = Foo();
result = temp.HasValue ? 
    new int?(A.op_implicit(Foo().Value)) : 
    y;

明らかにそれは間違っています。正しい下降は

result = temp.HasValue ? 
    new int?(A.op_implicit(temp.Value)) : 
    y;

これまでの分析に基づいた最良の推測は、nullableオプティマイザーがRailsから外れていることです。 nullable型の特定の式がnullにならない可能性があることがわかっている状況を探すnullableオプティマイザーがあります。次の単純な分析を検討してください。

result = Foo() ?? y;

と同じです

A? temp = Foo();
result = temp.HasValue ? 
    (int?) temp : 
    y;

そして、我々はそれを言うかもしれません

conversionResult = (int?) temp 

と同じです

A? temp2 = temp;
conversionResult = temp2.HasValue ? 
    new int?(op_Implicit(temp2.Value)) : 
    (int?) null

しかし、オプティマイザーが介入して、「ちょっと待って、すでにtempがnullでないことを確認しました。リフトされた変換演算子を呼び出しているからといって、再度nullを確認する必要はありません」と言うことができます。彼らはそれを最適化して

new int?(op_Implicit(temp2.Value)) 

私の推測では、(int?)Foo()の最適化された形式がnew int?(op_implicit(Foo().Value))であるという事実をどこかにキャッシュしているのではないかと考えています。 Foo()-replaced-with-temporary-and-then-convertedの最適化された形式が必要です。

C#コンパイラの多くのバグは、不適切なキャッシュ決定の結果です。賢明な言葉:後で使用するためにファクトをキャッシュするたびに、何か関連する変更が発生した場合に不整合が発生する可能性があります。この場合、初期分析後に変更された関連事項は、Foo()の呼び出しが常に一時的なフェッチとして実現されることです。

C#3.0では、nullableの書き換えパスの多くの再編成を行いました。このバグはC#3.0および4.0で再現されますが、C#2.0では再現されません。つまり、バグはおそらく私の悪いものでした。ごめんなさい!

データベースにバグを入力し、将来のバージョンの言語でこれを修正できるかどうかを確認します。分析にご協力いただきありがとうございます。とても役に立ちました!

更新:Roslynのヌル可能オプティマイザーをゼロから書き直しました。今ではより良い仕事をし、この種の奇妙なエラーを回避しています。 Roslynのオプティマイザーがどのように機能するかについてのいくつかの考えについては、ここから始まる一連の記事を参照してください。 https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/ =

411
Eric Lippert

これは間違いなくバグです。

public class Program {
    static A? X() {
        Console.WriteLine("X()");
        return new A();
    }
    static B? Y() {
        Console.WriteLine("Y()");
        return new B();
    }
    static C? Z() {
        Console.WriteLine("Z()");
        return new C();
    }

    public static void Main() {
        C? test = (X() ?? Y()) ?? Z();
    }
}

このコードは次を出力します:

X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)

そのため、各??合体式の最初の部分が2回評価されると思いました。このコードはそれを証明しました:

B? test= (X() ?? Y());

出力:

X()
X()
A to B (0)

これは、式で2つのNULL入力可能型間の変換が必要な場合にのみ発生するようです。辺の1つが文字列であるさまざまな順列を試しましたが、いずれもこの動作を引き起こしませんでした。

84
configurator

左グループ化されたケースで生成されたコードを見ると、実際には次のようなことを行います(csc /optimize-):

C? first;
A? atemp = a;
B? btemp = (atemp.HasValue ? new B?(a.Value) : b);
if (btemp.HasValue)
{
    first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value);
}

sefirstの場合、abの両方がnullでcを返す場合、ショートカットが生成されます。ただし、aまたはbがnullでない場合、aへの暗黙的な変換の一部としてBを再評価してから、aまたはbはnull以外です。

C#4.0仕様、§6.1.4から:

  • NULL可能変換がS?からT?への場合::
    • ソース値がnullHasValueプロパティがfalse)の場合、結果はT?型のnull値になります。
    • それ以外の場合、変換はS?からSへのアンラップ、それに続くSからTへの基礎となる変換、それに続くラップとして評価されます(§4.1.10) TからT?へ。

これは、2番目のアンラッピングとラッピングの組み合わせを説明しているようです。


C#2008および2010コンパイラは非常によく似たコードを生成しますが、これは上記の次のコードを生成するC#2005コンパイラ(8.00.50727.4927)からの回帰のように見えます。

A? a = x;
B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y;
C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z;

これは型推論システムに追加されたmagicによるものではないのでしょうか?

54
user7116

実際には、より明確な例でこれをバグと呼ぶことにします。これはまだ当てはまりますが、二重評価は確かに良くありません。

A ?? BA.HasValue ? A : Bとして実装されているようです。この場合、多くのキャストもあります(三項?:演算子の通常のキャストに続いて)。しかし、あなたがそれをすべて無視するなら、これはそれがどのように実装されているかに基づいて理にかなっています:

  1. A ?? BA.HasValue ? A : Bに展開されます
  2. Ax ?? yです。 x.HasValue : x ? yに展開します
  3. 出現するすべてのAを置き換えます-> (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

ここで、x.HasValueが2回チェックされ、x ?? yがキャストを必要とする場合、xが2回キャストされることがわかります。

コンパイラのバグではなく、単に??がどのように実装されているかの成果物として単純に書きました。 テイクアウェイ:副作用のある暗黙的なキャスト演算子を作成しないでください。

これは、??の実装方法をめぐるコンパイラのバグのようです。まとめ:副作用のある合体式をネストしないでください。

16
Philip Rieck

私は質問履歴からわかるように、C#の専門家ではありませんが、これを試してみましたが、バグだと思います...しかし、初心者として、私は行くことすべてを理解していないと言わざるを得ませんここにあるので、途中で出たら答えを削除します。

このbug結論に到達したのは、同じシナリオを扱うが、それほど複雑ではないプログラムの別のバージョンを作成することです。

バッキングストアで3つのnull整数プロパティを使用しています。それぞれを4に設定し、int? something2 = (A ?? B) ?? C;を実行します

ここに完全なコード

これは、Aのみを読み取ります。

私にとって、この声明は次のように見えます。

  1. 大括弧で始め、Aを見て、Aを返し、Aがヌルでない場合は終了します。
  2. Aがヌルの場合、Bを評価し、Bがヌルでない場合は終了します
  3. AとBがヌルの場合、Cを評価します。

したがって、Aはnullではないため、Aだけを見て終了します。

あなたの例では、最初のケースにブレークポイントを置くことは、x、y、zがすべてnullではないことを示しているので、それらは私のそれほど複雑でない例と同じように扱われることを期待します.... C#の初心者であり、この質問のポイントを完全に逃しました!

10
Wil