C#中問號(?)用法總結
三元運算子(?:)
三元運算子應該都很熟,通常我們也可以使用if-else來代替三元運算,這一點就不多說了,一個簡單的例子:
//取一個0.5-1之間的隨機值 var value = new Random().NextDouble(); value = value < 0.5 ? 0.5 + value : value;
可為空的值型別(T?)
C#中的值型別指的是結構體型別和列舉型別,他們有一個特點,就是擁有預設值(有時候也叫零值),而且它們不允許被賦予null值,因此C#在此基礎上,引入了可空的值型別:
//T是值型別,T?本質上是Nullable<T>的簡寫,可以認為是T值型別加上null的組合型別T?
可空值型別是Nullable<T>的物件,可以認為是T值型別加上null的組合型別,比如bool?,表示它的值只能是true、false或者null。
需要注意,Nullable<T>也是值型別,但是於普通的值型別不一樣,它在未初始化時並非是你泛型引數的預設值,而是null,我們也可以認為Nullable<T>的預設值是null。
Nullable<T>有兩個很常用的屬性:
HasValue:表示當前可空值型別是否有值,如果有值(即不等於null),那麼返回true,否則返回false Value:如果HasValue=true,則返回這個可空值型別對應的那個值型別型別的值,否則丟擲 InvalidOperationException。
值運算的一元運算子和二元運算子也使用於可空值型別,當運算子所有運算物件都不為null時,它就等價於運算子作用於它們對應的值型別物件,否則返回null,也就是說可空型別的一元運算子和二元運算子返回的也是可空型別:
int? a = 10; int? b = null; int? c = 10; a++; //等價於a=a+1 b++; //等價於b=b+1,因為b=null,因此自增後b還是nullvar d = a * c; //雖然a和c都不等於null,但d還是可空型別 var e = a + b; //因為b=null,因此e=null
注:bool?的 & 和 | 運算時,不遵循這個規則,詳細見如下:
x | y | x & y | x|y |
true | true | true | true |
true | false | false | true |
true | null | null | true |
false | true | false | true |
false | false | false | false |
false | null | false | null |
null | true | null | true |
null | false | false | null |
null | null | null | null |
可空引用型別(T?)
可空引用型別是為可空上下文而生的,具體可以參考:C# 可空上下文、可空引用型別(?)與可空容忍(!)
null 條件運算子(?.和?[])
null 條件運算子是一種成員訪問符的拓展,從C#6.0開始出現:
?.表示成員訪問運算子,主要用於物件屬性、方法等成員訪問,?[]表示元素訪問運算子,主要用於集合中元素訪問
null 條件運算子主要用於簡化我們的程式碼:
a?.b 或 a?[b]的運算邏輯是: 1、如果a是null,那麼a.b和a[b]都會丟擲空指標異常,而a?.b和a?[b]都返回null,而不會丟擲異常 2、如果a不是null,那麼a?.b和a?[b]等價於a.b和a[b],如果a.b和a[b]丟擲異常,a?.b和a?[b]也會丟擲相同的異常
例如:
static void Method(Exception exception,string[] array) { //這裡我們不知道exception和array會不會是null,所以我們用它前做if判斷,否則會丟擲空指標異常 if (exception != null) { Console.WriteLine(exception.Message); } if (array != null) { Console.WriteLine(array[0]); } //可以簡寫成 Console.WriteLine(exception?.Message); Console.WriteLine(array?[0]); }
需要注意的是,null 條件運算子的返回值是可空型別,如果我們原本值是值型別,那麼經過null 條件運算子將會變成可空的值型別。例如:
//Length是int型別的值型別 int? length=array?.Length;//經過null 條件運算子,變成了可空的值型別
特別是在級聯式呼叫時,null 條件運算子會很有用:
A?.B?.C?.D() E?.F?.G?[0]
這樣,我們就不需要寫很多的if判斷了。
其實很多時候,我們是將null 條件運算子和null 合併運算子(??)一起使用的。
null 合併運算子(??和??=)
合併運算子(??)比較簡單,它表示:
expr1 ?? expr2 1、如果表示式expr1的結果是null,那麼會計算表示式expr2,並返回表示式expr2的結果 2、如果表示式expr1的結果不是null,那麼會直接返回表示式expr1的結果,此時並不會計算表示式expr2的結果
例如:
List<int> numbers = null; int? a = null; //如果numbers=null,則重新初始化一個List,這樣不用使用if判斷了,list始終不為null var list = numbers ?? new List<int>(); list.Add(a ?? 1);//如果a=null,則返回1 int? m = 1, n = 2, k = 10; var i = (m + n) ?? ++k;//因為m+n!=null,所以++k並不會被計算 Console.WriteLine(k);//10
合併運算子(??)常常和null 條件運算子(?.),用於實現一種實現預設值得情況:
public class MyClass { public int Value { get; set; } } void Method(MyClass myClass) { //如果只是用?.運算,那麼value將會值int?型別,這時可以結合??運算,value就會是int型別 var value = myClass?.Value ?? 1;//相當於說:如果myClass=null,那麼myClass?.Value返回null,??就會返回1給value變數 }
此外,合併運算子(??)也可以類似級聯的使用,與null 條件運算子(?.)一起使用時,可能會有意想不到的簡潔:
//??合併運算子類似級聯 var a = A ?? B ?? C ?? D ?? default; //與null 條件運算子(?.)一起使用,想想如果是使用if會有多麼繁瑣 var b = A?.B?.C() ?? D?.E?.F() ?? G?.H?.I() ?? default;
合併運算子(??)還經常和throw表示式結合,用於簡化空指標判斷,它常出現在建構函式和屬性構造器中:
public class MyClass { public MyClass(string name) { this.name = name ?? throw new ArgumentNullException(nameof(name)); } string name; public string Name { get => name; set => name = value ?? throw new ArgumentNullException(nameof(value)); } }
??=也稱為合併賦值運算子,是合併運算子(??)的一個特殊情況,它類似於+=、-=等運算,用於簡化賦值運算:
a ??= b //等價於 a = a ?? b //等價於 if (a is null) { a = b; }
同樣的,??=也具有賦值運算子的一些特性,如:
a ??= b ??= c
總結
有關問號(?)的這些語法糖可以方便我們的開發,而且像ECMAScript也提出可選鏈操作符(?.)和空值合併運算子(??)的語法,所以這些語法糖是值得我們學習。
此外,不知道你發現沒有,除了三元運算子,包含?的運算子多多少少都和null有關,可能以後還會出現和?結合的運算子,但是不出意外的話也會和null有關吧。
參考文件:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator
一個專注於.NetCore的技術小白