從 “x is null 和 x == null” 的區別看 C# 7 模式匹配中常量和 null 的匹配


 

嘗試過寫 if (x is null)?它與 if (x == null) 相比,孰優孰劣呢?

x is null 還有 x is constant 是 C# 7.0 中引入的模式匹配(Pattern Matching)中的一個小細節。閱讀本文將了解 x is constantx == constant 之間的差別,並給出一些代碼編寫建議。


 

C# 7 的模式匹配

說到 C# 中新增的模式匹配,想必大家一定不會忘了變量的匹配。以下例子來自於微軟官方 C# 7.0 的介紹文檔 What’s New in C# 7 - C# Guide - Microsoft Docs

public static int DiceSum2(IEnumerable<object> values)
{
    var sum = 0;
    foreach(var item in values)
    {
        if (item is int val)
            sum += val;
        else if (item is IEnumerable<object> subList)
            sum += DiceSum2(subList);
    }
    return sum;
}
public static int DiceSum3(IEnumerable<object> values)
{
    var sum = 0;
    foreach (var item in values)
    {
        switch (item)
        {
            case int val:
                sum += val;
                break;
            case IEnumerable<object> subList:
                sum += DiceSum3(subList);
                break;
        }
    }
    return sum;
}

其實,官方文檔中也順帶提及了常量的匹配:

public static int DiceSum5(IEnumerable<object> values)
{
    var sum = 0;
    foreach (var item in values)
    {
        switch (item)
        {
            case 0:
                break;
            case int val:
                sum += val;
                break;
            case PercentileDie die:
                sum += die.Multiplier * die.Value;
                break;
            case IEnumerable<object> subList when subList.Any():
                sum += DiceSum5(subList);
                break;
            case IEnumerable<object> subList:
                break;
            case null:
                break;
            default:
                throw new InvalidOperationException("unknown item type");
        }
    }
    return sum;
}

然而,微軟居然只在 switch-case 里面說了常量的匹配,而且 case 0case null 這不本來就是我們以前熟悉的寫法嗎!(只不過以前只能判斷一個類型的常量)


x is null Vs. x == null

好了,回到正題。我們想說的是 x is nullx == null。為了得知它們的區別,我們寫一段代碼:

private void TestInWalterlvsDemo(object value)
{
    if (value is null)
    {
    }
    if (value == null)
    {
    }
}

反編譯看看:

.method private hidebysig instance void 
    TestInWalterlvsDemo(
      object 'value'
    ) cil managed 
{
    .maxstack 2
    .locals init (
      [0] bool V_0,
      [1] bool V_1
    )

    // [37 9 - 37 10]
    IL_0000: nop          

    // [38 13 - 38 31]
    IL_0001: ldarg.1      // 'value'
    IL_0002: ldnull       
    IL_0003: ceq          
    IL_0005: stloc.0      // V_0

    IL_0006: ldloc.0      // V_0
    IL_0007: brfalse.s    IL_000b

    // [39 13 - 39 14]
    IL_0009: nop          

    // [40 13 - 40 14]
    IL_000a: nop          

    // [41 13 - 41 31]
    IL_000b: ldarg.1      // 'value'
    IL_000c: ldnull       
    IL_000d: ceq          
    IL_000f: stloc.1      // V_1

    IL_0010: ldloc.1      // V_1
    IL_0011: brfalse.s    IL_0015

    // [42 13 - 42 14]
    IL_0013: nop          

    // [43 13 - 43 14]
    IL_0014: nop          

    // [44 9 - 44 10]
    IL_0015: ret          

} // end of method MainPage::Test

x is null 對應的是:

IL_0001: ldarg.1 // 'value'
IL_0002: ldnull 
IL_0003: ceq 
IL_0005: stloc.0 // V_0

ldarg.1 將第 1 號參數壓到評估棧(為什么不是第 0 號?因為第 0 號是 this)。然后將 ldnullnull 壓到評估棧上。隨后,ceq 比較壓入的兩個值是否相等。(注意是比較棧中的值哦,不會看引用的對象的!所以如果是引用類型,則比較的是引用本身哦,類似於指針!) 此處划重點,因為考試要考!咳咳……哦不,是后面要用到……

x == null 對應的是:

IL_000b: ldarg.1 // 'value'
IL_000c: ldnull 
IL_000d: ceq 
IL_000f: stloc.1 // V_1

於是發現兩個完全一樣!!!-_- 本文完,全劇終。


x is 常量 Vs. x == 常量

如果只是像上面那樣,那這篇文章也太沒營養了!現在我們把 null 換成其它常量:

private void TestInWalterlvsDemo(object value)
{
    if (value is 1)
    {
    }
    if (value == 1)
    {
    }
}

呀……編譯不通過!改改……

private void TestInWalterlvsDemo(object value)
{
    if (value is 1)
    {
    }
    if (value == (object) 1)
    {
    }
}

於是再看看反編譯出來的結果。

value is 1

IL_0001: ldc.i4.1 
IL_0002: box [mscorlib]System.Int32
IL_0007: ldarg.1 // 'value'
IL_0008: call bool [mscorlib]System.Object::Equals(object, object)
IL_000d: stloc.0 // V_0

value == (object) 1

IL_0013: ldarg.1 // 'value'
IL_0014: ldc.i4.1 
IL_0015: box [mscorlib]System.Int32
IL_001a: ceq 
IL_001c: stloc.1 // V_1

現在已經不一樣了,前者再比較時用的是 call,調用了 bool [mscorlib]System.Object::Equals(object, object) 方法;而后者依然用的是 ceq

區別已經很明顯了,前者會根據具體類型具體判斷相等,也就是說引用類型會調用引用類型自己的方法判斷相等,值類型也會調用值類型的方法判斷相等。而后者依然是比較評估棧中的兩個值是否相等。關鍵是這兩者均出現了裝箱!也就是說——因為裝箱的存在,對后者而言,ceq 會壓入 0,即永遠返回 false,這就是 BUG 所在。這就是不一樣的地方!


回顧模式匹配中的常量匹配

在 C# 7 的模式匹配中,null 和常量其實都一樣是常量,本來都是會調用 Object.Equals(object, object) 靜態方法進行比較的;但 null 因為其特殊性,被編譯器優化掉了,於是 x is nullx == null 完全一樣;x is constantx == constant 依然有區別。

從反編譯的 MSIL 代碼中我們也可以得出一些代碼編寫上的建議。在比較常量的時候,如果可能,盡量使用 is 進行比較,而不是 ==。好處多多:

  • 如果是 null,寫 x is null 很符合英語的閱讀習慣,代碼閱讀起來比較舒適。
  • 如果是值常量,可以避免裝箱帶來的相等判斷錯誤問題

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM