興味深い null 合体演算子のカスタム暗黙的な変換動作 質問する

興味深い null 合体演算子のカスタム暗黙的な変換動作 質問する

注: これは修正されたようですロザリン

この疑問は、私が回答を書いているときに浮かびました。これですの結合性について述べている。ヌル合体演算子

念のため、ヌル合体演算子の考え方は、次の形式の式である。

x ?? y

まず を評価しx、次に次の操作を実行します。

  • の値がxnullの場合、yが評価され、それが式の最終結果となる。
  • の値xがnullでない場合、yは評価されず、 の値は、必要に応じてxコンパイル時の の型に変換された後の式の最終結果です。y

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

の単純なケースでは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;
    }
}

したがって、 A から B、A から C、B から C への変換を伴う3 つのカスタム値タイプ、 ABがあります。C

2 番目のケースと 3 番目のケースはどちらも理解できますが、最初のケースではなぜA から B への変換が追加で行われるのでしょうか。特に、最初のケースと 2 番目のケースは同じものであると予想していました。結局のところ、式をローカル変数に抽出しているだけなのです。

何が起こっているのか、誰か理解できますか? C# コンパイラに関しては「バグ」と叫ぶのは非常にためらいますが、何が起こっているのか困惑しています...

編集: さて、configurator の回答のおかげで、何が起こっているのかを示すさらに厄介な例がここにあります。これにより、バグであると考える理由がさらに増えました。編集: このサンプルでは、​​2 つの null 結合演算子さえ必要ありません...

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

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

ベストアンサー1

この問題の分析にご協力いただいた皆様に感謝します。これは明らかにコンパイラのバグです。合体演算子の左側に 2 つの null 許容型を含むリフト変換がある場合にのみ発生するようです。

どこで問題が起きるのか正確には特定できていませんが、コンパイルの「null許容値を下げる」段階(初期分析後、コード生成前)のどこかの時点で、式を縮小します。

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;

これまでの分析に基づく私の推測では、ここではヌル可能オプティマイザが軌道から外れているようです。ヌル可能オプティマイザは、ヌル可能型の特定の式がヌルになる可能性がないとわかっている状況を探します。次の素朴な分析を考えてみましょう。まず、次のように言うかもしれません。

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を2度確認する必要はありません」と言うことができます。私たちはそれを最適化して、

new int?(op_Implicit(temp2.Value)) 

私の推測では、 の最適化された形式が で(int?)Foo()あるという事実をどこかにキャッシュしnew int?(op_implicit(Foo().Value))ていますが、それは実際に必要な最適化された形式ではありません。必要なのは、 Foo() を一時的に置き換えてから変換した最適化された形式です。

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

C# 3.0 では、nullable 書き換えパスを大幅に再編成しました。このバグは C# 3.0 と 4.0 では再現されますが、C# 2.0 では再現されません。つまり、このバグはおそらく私のミスです。申し訳ありません!

バグをデータベースに入力して、言語の将来のバージョンでこれを修正できるかどうかを確認します。分析していただいた皆様に改めて感謝します。非常に役に立ちました。

更新: Roslyn 用に nullable オプティマイザーを最初から書き直しました。今では、より優れた機能を発揮し、このような奇妙なエラーを回避しています。Roslyn のオプティマイザーの動作に関する考察については、ここから始まる私の一連の記事を参照してください。https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/

おすすめ記事