567

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

この質問は、 null 合体演算子の結合性について説明している this oneに対する私の回答を書いているときに発生しました。

念のために言っておきますが、null 合体演算子の考え方は、次の形式の式であるということです。

x ?? y

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

  • の値xが null の場合、yが評価され、それが式の最終結果になります
  • の値がxnull 以外で、yが評価され、 の値が式の最終結果である場合、必要に応じxてコンパイル時の型 に変換されます。y

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

の単純なケースでは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 への変換を伴うABおよびの 3 つのカスタム値タイプがあります。C

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

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

編集:さて、構成者の答えのおかげで、何が起こっているかのより厄介な例がここにあります。これは、それがバグだと考えるさらなる理由を与えてくれます。編集: サンプルは 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()れる理由がわかりません。

4

5 に答える 5

429

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

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

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 オプティマイザーがここで軌道から外れているということです。null 許容型の特定の式が null になる可能性がないことがわかっている状況を探す、null 許容オプティマイザがあります。次の素朴な分析を考えてみましょう。

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()isであるという事実をどこかにキャッシュしていますnew int?(op_implicit(Foo().Value))が、それは実際には必要な最適化された形式ではありません。最適化された形式の Foo()-replaced-with-temporary-and-then-converted が必要です。

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

C# 3.0 では、null 許容書き換えパスを大幅に再編成しました。このバグは C# 3.0 および 4.0 では再現されますが、C# 2.0 では再現されません。ごめん!

データベースにバグを入力して、言語の将来のバージョンでこれを修正できるかどうかを確認します。皆さんの分析に感謝します。とても助かりました!

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

于 2011-06-07T21:01:09.797 に答える
84

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

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 つが文字列であるさまざまな順列を試しましたが、いずれもこの動作を引き起こしませんでした。

于 2011-06-06T20:17:41.707 に答える
55

左グループのケースで生成されたコードを見ると、実際には次のようなことが行われます ( 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);
}

もう 1 つの検索結果は、使用 すると、との両方が null で returnfirstの場合にショートカットが生成されます。ただし、orが null 以外の場合は、 null 以外のorを返す前に、への暗黙的な変換の一部として再評価されます。abcabaBab

C# 4.0 仕様、§6.1.4 から:

  • nullable 変換が からS?への場合T?:
    • ソース値がnull( HasValueproperty is false) の場合、結果はnulltype の値になりますT?
    • S?それ以外の場合、変換は からへのアンラッピングとして評価されS、その後に から への基本的な変換が続き、その後に からSTのラッピング (§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;

これは、型推論システムに与えられた追加の魔法によるものではないのだろうか?

于 2011-06-06T19:15:23.290 に答える
17

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

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

  1. A ?? B に拡大A.HasValue ? A : B
  2. A私たちの x ?? yです。に展開x.HasValue : x ? y
  3. A->のすべての出現を置き換えます (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

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

??コンパイラのバグではなく、実装方法の成果物として単純に説明します。 持ち帰り:副作用のある暗黙のキャスト演算子を作成しないでください。

??これは、実装方法を中心に展開するコンパイラのバグのようです。要点:副作用のある合体式をネストしないでください。

于 2011-06-06T19:40:28.573 に答える
11

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

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

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

(完全なコードはこちら)

これは A だけを読み取り、それ以外は何も読み取りません。

このステートメントは、私には次のように見えます。

  1. 角かっこで開始し、A を見て、A を返し、A が null でない場合は終了します。
  2. A が null の場合は B を評価し、B が null でない場合は終了します
  3. A と B が null の場合、C を評価します。

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

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

于 2011-06-06T20:34:45.573 に答える