1. ホーム
  2. c#

[解決済み] NULL-COALESCING 演算子のカスタム暗黙変換の不思議な挙動

2022-03-17 04:25:04

質問

注:これは、以下のように修正されたようです。 ロスリン

への回答を書いていて、この疑問がわいた。 これ の連想性について述べています。 ヌルコアレスティング演算子 .

注意点として、null-coalescing演算子の考え方は、以下のような形の式になります。

x ?? y

を最初に評価します。 x , その後

  • の値が x がヌルである場合。 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;
    }
}

というわけで、3つのカスタムバリューのタイプができました。 A , BC を、AからB、AからC、BからCに変換しています。

2つ目のケースも3つ目のケースも理解できる...が なぜ は、最初のケースで余分なA-B変換があるのでしょうか?特に、私は 本当に 最初のケースと2番目のケースは同じものであることを期待していました。

何が起こっているのか、誰かわかる人はいますか?C#コンパイラーに関しては、バグと叫ぶのは非常にためらわれるのですが、何が起こっているのかわからずに困っています...。

EDIT: さて、configuratorの回答のおかげで、これがバグだと思う根拠がさらに増えました。EDIT: このサンプルでは、2つのnull-coalescing演算子さえ必要ありません...

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回呼ばれるのは、私にとって非常に驚くべきことです。 評価 を2回実行します。

解決方法は?

この問題の解析にご協力いただいた皆様、ありがとうございました。これは明らかにコンパイラのバグです。合体演算子の左辺に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;

ここまでの分析から推測すると、NULL可能なオプティマイザがここでレールから外れているのだと思います。nullableオプティマイザは、nullable型の特定の式が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でないことはすでにチェックしたんだ。 私たちは、それを最適化して、単に

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では再現しないので、おそらく私のミスであることがわかる。申し訳ありません。

データベースにバグを入力してもらい、今後の言語のバージョンアップで修正できるかどうか確認します。皆さん、本当にありがとうございました。

UPDATE: Roslyn用にnullableオプティマイザを一から書き直しました。より良い仕事をし、この種の奇妙なエラーを回避できるようになりました。Roslynのオプティマイザがどのように動作するかについての考察は、ここから始まる連載記事をご覧ください。 https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/