嘗試過寫 if (x is null)
?它與 if (x == null)
相比,孰優孰劣呢?
x is null
還有 x is constant
是 C# 7.0 中引入的模式匹配(Pattern Matching)中的一個小細節。閱讀本文將了解 x is constant
和 x == 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 0
、case null
這不本來就是我們以前熟悉的寫法嗎!(只不過以前只能判斷一個類型的常量)
x is null
Vs. x == null
好了,回到正題。我們想說的是 x is null
和 x == 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
)。然后將 ldnull
將 null
壓到評估棧上。隨后,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 null
和 x == null
完全一樣;x is constant
和 x == constant
依然有區別。
從反編譯的 MSIL 代碼中我們也可以得出一些代碼編寫上的建議。在比較常量的時候,如果可能,盡量使用 is
進行比較,而不是 ==
。好處多多:
- 如果是
null
,寫x is null
很符合英語的閱讀習慣,代碼閱讀起來比較舒適。 - 如果是值常量,可以避免裝箱帶來的相等判斷錯誤問題
參考資料
- What’s New in C# 7 - C# Guide - Microsoft Docs
- Dissecting the pattern matching in C# 7 – Dissecting the code
- c# - What is the difference between “x is null” and “x == null”? - Stack Overflow
- C# 7.0 語言新特性 - 技術翻譯 - 開源中國社區
- OpCodes.Ceq Field (System.Reflection.Emit)
- OpCodes.Ldarg_0 Field (System.Reflection.Emit)
- OpCodes.Stloc Field (System.Reflection.Emit)
- OpCodes.Ldc_I4_1 Field (System.Reflection.Emit)