question - ternary operator c#




好奇的空合併運算符自定義隱式轉換行為 (4)

注意:這似乎已在Roslyn修復

這個問題出現在我寫這個答案的時候,它談到了空合併算子的關聯性。

提醒一下,空合併運算符的想法是表單的形式

x ?? y

首先評估x ,然後:

  • 如果x值為null,則評估y並且這是表達式的最終結果
  • 如果x的值非空, 則不計算y ,並且在必要時將y轉換為編譯時類型之後, x的值是表達式的最終結果

現在通常不需要轉換,或者它只是從可空類型轉換為不可空的類型 - 通常類型是相同的,或者僅僅來自(比如說) 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;
    }
}

所以我們有三種自定義值類型ABC ,轉換從A到B,A到C和B到C.

我可以理解第二種情況和第三種情況......但為什麼在第一種情況下會有額外的A到B轉換? 特別是,我真的期望第一種情況和第二種情況是相同的 - 畢竟只是將表達式提取到局部變量中。

任何參與者正在發生什麼? 當談到C#編譯器時,我對於“錯誤”非常抱歉,但我很難理解發生了什麼......

編輯:好吧,這是一個很糟糕的例子,感謝配置器的答案,這給了我更多的理由認為它是一個錯誤。 編輯:示例甚至不需要兩個空合併操作符...

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()在這裡被調用兩次這一事實對我來說是非常令人驚訝的 - 我看不出有任何理由讓表達式被評估兩次。


其實,現在我會用這個更清晰的例子來稱呼這個bug。 這仍然成立,但雙重評價當然不好。

看起來好像A ?? B A ?? B被實現為A.HasValue ? A : B A.HasValue ? A : B 。 在這種情況下,也有很多鑄造(在三元?:運算符的常規鑄造之後)。 但是,如果你忽略所有這些,那麼這基於它的實現方式是有意義的:

  1. A ?? B A ?? B擴展到A.HasValue ? A : B A.HasValue ? A : B
  2. A是我們的x ?? y x ?? y 。 展開到x.HasValue : x ? y x.HasValue : x ? y
  3. 替換所有出現的A - > (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

在這裡你可以看到x.HasValue被選中了兩次,如果x ?? y x ?? y需要施放, x會施放兩次。

我會把它簡單地歸結為一個神器?? 被實現,而不是編譯器錯誤。 帶走:不要創建帶有副作用的隱式轉換運算符。

這似乎是一個編譯器錯誤周圍旋轉?? 已實施。 外賣:不要嵌套凝聚式表達與副作用。


如果您查看左分組情況的生成代碼,它實際上會做類似這樣的事情( 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);
}

另一個發現,如果您first 使用 ,它將生成一個快捷方式,如果ab都為空並返回c 。 然而,如果ab非空,它會在返回ab中的非空值之前重新評估a作為隱式轉換為B一部分。

從C#4.0規範,第6.1.4節:

  • 如果可空轉換來自S? T?
    • 如果源值為nullHasValue屬性為false ),那麼結果是類型T?nullT?
    • 否則,轉換被評估為從S?展開S?S ,然後是從ST的底層轉換,然後是從TT?的換行(第4.1.10節) T?

這似乎解釋了第二個解包裹組合。

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;

我想知道這是不是由於類型推理系統的額外魔法


感謝所有分析這個問題的人。 這顯然是一個編譯器錯誤。 它似乎只發生在合併運算符左側的兩個可空類型的提升轉換時。

我還沒有確定出錯的地方,但是在編譯的“可空的降低”階段的某個時刻 - 在初始分析之後,代碼生成之前 - 我們減少了表達式

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的情況。 考慮下面的天真分析:我們可以先說

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()的優化形式 - 用臨時和隨後轉換替換。

C#編譯器中的許多錯誤都是由於緩存決定不當造成的。 對智者說的一句話: 每次你緩存一個事實供以後使用時,如果有相關的變化,你可能會造成不一致 。 在這種情況下,改變了後期初始分析的相關事件是,對Foo()的調用應該總是作為臨時獲取來實現。

我們在C#3.0中進行了很多可重寫的重寫傳輸的重組。 該錯誤在C#3.0和4.0中重現,但不在C#2.0中重現,這意味著該錯誤可能是我的錯誤。 抱歉!

我會收到一個輸入到數據庫中的錯誤,我們會看看我們是否可以修復這個語言的未來版本。 再次感謝大家的分析。 這是非常有益的!

更新:我重寫了Roslyn的可空優化器; 它現在做得更好,避免了這些奇怪的錯誤。 有關Roslyn中優化器如何工作的一些想法,請參閱我的一系列文章,這些文章從這裡開始: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/


這絕對是一個錯誤。

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)

這讓我覺得每個人的第一部分?? coalesce表達式被評估兩次。 這段代碼證明了它:

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

輸出:

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

這似乎只在表達式需要在兩個可為空的類型之間進行轉換時才會發生; 我嘗試了各種排列方式,其中一個方面是一個字符串,並沒有一個導致這種行為。





null-coalescing-operator