為甚麼值類型變量不能是空
非空引用值提供一個訪問對象的路徑,null引用等同「我不引用任何對象」
在內存空間中,引用類型的表示是全零(引用地址)。本質上與其他引用的存儲方式是一樣的。
值本身由一個字節組成
可以將值0-255存儲到變量中。
如果我們在這基礎上加一個位元表示null,總共就要257個位,沒有辦法用一個字節存儲這麼多的值。
C# 1表示空值的方法
魔值
犧牲一個值來表示空值,如Int.MinValue
魔值不浪費任何內存
同時該值將永遠不能被用來表示真正的數據
引用類型包裝
方法一:用object作用變量類型,根據需要進行裝箱/拆箱
方法二:假定值類型A可空,就為它準備一個引用類型B。在引用類型B中,包含值類型A的一個實例變量,並在B中聲明一個隱式轉換操作符。
允許直接使用null
要求在堆上創建對象,可能導致GC困難、內存消耗增加
額外的bool
將值和bool封裝到另一個值類型中
由於也是值類型,因此可以避免GC
通過封裝的值內表示可空性,而不是通過空引用表示
針對每一個值類型創建一個新的類型
如果值因為某種原因要進行裝箱,那不管它是否被認為是空值,都要像平時那樣進行裝箱
System.Nullable(泛型)
public struct Nullable<T> where T : struct
{
public Nullable(T value);
public bool HasValue { get; }
public T Value { get; }
[NullableContextAttribute(2)]
public override bool Equals(object? other);
public override int GetHashCode();
public T GetValueOrDefault();
public T GetValueOrDefault(T defaultValue);
[NullableContextAttribute(2)]
public override string? ToString();
public static implicit operator T?(T value);
public static explicit operator T(T? value);
}Nullable(可空類型)是一個泛型類型
對於任何具體可空類型而言,T的類型稱為可空類型的基礎類型(underlying type)
Nullable的基礎類型為int
Nullable<T>仍然是值類型,所以實例仍然是在棧上
在C#中,在值類型後跟上”?”使之等價於對應的可空值類型
int? a;
Nullable<int> a;以上兩者相互等價
Nullable的屬性
HasValue
Value
另外,由於Nullable仍然為值類型,因此對於Nullable類型的變量來說,其值將直接包含一bool(HasValue)和 int(Value),而不會是其他對象的引用
Nullable的方法
構造方法:可指定值決定要不要創建一個有值的實例
GetValueOrDefault:如果實例存在值,就返回該值,否則返回一個默認值
GetHashCode/ToString等重載
GetHashCode在沒有值時返回0
ToString在沒有值時設回空字符串
轉換方法
非可空類型T轉換到可空類型T的隱式轉換
轉換結果為一個HasValue == true的實例
可空類型T轉到到非可空類型T的顯式轉換
沒有可以返回的Value時拋出異常
將T的實例轉換成Nullable的實例稱為「包裝」
int a = 5;
Nullable<int> b = a;
將Nullable的實例轉換為T的實例稱為「拆包」
Nullable<int> a = 5;
int b = (int)a;
比較方法
來自靜態類Nullable(非泛型)的兩個靜態方法
對於沒有值的實例,比較方法的返回值遵從.NET的約定:空與空相等;空小於所有值
public static int Compare<T>(Nullable<T> n1, Nullable<T> n2) //使用Comparer.Default
public static bool Equals<T>(Nullable<T> n1, Nullable<T> n2) //使用EqualityComparer.Default
來自Nullable(非泛型)的一個支持
public static Type GetUnderlyingType(Type nullableType)
如果參數是可空類型,方法返回其基礎類型,否則返回null
Nullable(泛型)的裝箱和拆箱
Nullable是一個struct(值類型),因此在進行引用類型操作時需要進行裝/拆箱
Nullable的實例裝箱
在沒有值時裝箱成空引用
在有值時,等同將其值(非可空)裝箱
Nullable裝箱 = T裝箱
拆箱:已裝箱的值可拆箱成普通類型/對應的可空類型
拆箱一個空引用時
只能拆成可空類型,否則報錯
空引用拆成可空類型後,會拆成一個沒有值的實例
如果對已裝箱值類型的引用是null,而且要把它拆箱成一個Nullable<T>,CLR會將Nullable<T>的值設為null
Nullable(泛型)實例的Equals(object)
設有一可空值類型first,一可空/非可空值類型seconds,first.Equals(second)在不同情況下的結果:
不必考慮second是否一個Nullable,基於Equals(object)的參數類型object將會對second進行裝箱。
當second有值時,會裝箱成一個非可空值類型的箱子
當second沒有值時,會返回一個空引用
if(first.HasValue && second == null) => first == second
if(first.HasValue && second != null) => first != second
if(!first.HasValue && second == null) => first != second
if(first.Value == second) => first == second
語法糖
- ? 修飾符
使用?修飾符修飾的值類型變量與使用Nullable聲明的變量會被編譯成同樣的IL(System.Nullable`1[T])
Nullable<int> a = 10; //IL : System.Nullable`1[System.Int32]
int? a = 10; //IL : System.Nullable`1[System.Int32]
- 使用null進行賦值和比較
設有一個Person類
類中有一個可空類型:死亡日期
static void Main(string[] args)
{
Person turing = new Person("Alan Turing", new DateTime(1912, 6, 23), new DateTime(1954, 6, 7));
//將null當作可空類型的實例來傳遞時,實際上是通過調用不可空類型的構造函數,為這個類型創建空值
Person knuth = new Person("Donald Knuth", new DateTime(1938, 1, 10), null);
Console.ReadKey();
}
public class Person
{
DateTime birth;
DateTime? death;
string name;
public TimeSpan Age
{
get
{
//當將可空變量同null進行比較時,實際上是在裡面的hasValue屬性
if (death == null) { return DateTime.Now - birth; }
else { return death.Value - birth; }
}
}
public Person(string name, DateTime birth, DateTime? death)
{
this.name = name;
this.birth = birth;
this.death = death;
}
}
對IL代碼的觀察
//if(death == null)
//調用HasValue屬性檢查death是否為空
IL_0007: call instance bool valuetype [System.Runtime]System.Nullable`1<valuetype [System.Runtime]System.DateTime>::get_HasValue()//在將null作為DateTime?類型的實參傳遞時
//Person knuth = new Person("Donald Knuth", new DateTime(1938, 1, 10), null);
//實際上是調用Nullable的默認構造方法(Initobj)
//有參構造方法的調用是newobj
IL_003e: initobj valuetype [System.Runtime]System.Nullable`1<valuetype [System.Runtime]System.DateTime>IL_0001: ldstr "Alan Turing"
IL_0006: ldc.i4 0x778 //1912
IL_000b: ldc.i4.6 //6
IL_000c: ldc.i4.s 23 //23
IL_000e: newobj instance void [System.Runtime]System.DateTime::.ctor(int32,
int32,
int32)
IL_0013: ldc.i4 0x7a2 //1954
IL_0018: ldc.i4.6 //6
IL_0019: ldc.i4.7 //7
//創建普通的DateTime構造函數
IL_001a: newobj instance void [System.Runtime]System.DateTime::.ctor(int32,
int32,
int32)
//將上面的結果傳給下面這條有一個參數的Nullable的構造函數中
IL_001f: newobj instance void valuetype [System.Runtime]System.Nullable`1<valuetype [System.Runtime]System.DateTime>::.ctor(!0)
IL_0024: newobj instance void CSharpInDepth.Person::.ctor(string,
valuetype [System.Runtime]System.DateTime,
valuetype [System.Runtime]System.Nullable`1<valuetype [System.Runtime]System.DateTime>)
- 可空轉換和操作符
假如一個非可空的值類型支持一個操作符或一種轉換,而且那個操作符或者轉換只涉及其他非可空類型時,那麼可空的值類型也支持相同的操作符或轉換
并且通常是將非可空的值類型轉換成它們的可空等價物
可空轉換
已知的可空轉換
null -> T?(隱式)
T -> T?(隱式)
T? -> T(顯式)
根據上述定義,以非可空類型int和long為例,他們之間有一系列的轉換操作,同樣地,可空類型int?和long?也有這些操作(這些涉及可空類型的轉換稱為「提升轉換」(lifted conversion)。
S? -> T? (顯/隱,取決於原始轉換)=>可空轉可空
S -> T?(顯/隱,取決於原始轉換)=>不可空轉可空
S? -> T(顯)=>可空轉不可空
可空操作符
- 當非可空的值類型T重載了操作符,可空類型T?將自動擁有相同的操作符,但操作數和結果類型稍有不同(這些操作符稱為「提升操作符」)
這些操作符的使用存在一些限制
true/false操作符永遠不會被提升
只有操作數是非可空值類型的操作符才會被提升
對於一元和二元操作符(相等和關係操作符除外),返回類型必須是一個非可空的值類型
對於相等和關係操作符,返回類型必須是bool
應用於bool?的&和|操作符有單獨定義的行為
int? four = 4;
int? five = 5;
int? nullInt = null;
//Rules
//對於所有操作符,操作數的類型都成為它們的可空等價物。
//對於一元和二元操作符,返回類型也為可空類型
//對於相等和關係操作符,返回類型為非可空bool
//如果任何一個操作數是空值,就返回一個空值
//進行相等測試時,兩個空值被認為相等
//進行相等測試時,空值和任何非空值被認為不相等
//對於關係操作符,任何一個操作數為空值,返回始終為false
//如果沒有空值操作數,自然使用非可空類型的操作符
Console.WriteLine(-nullInt); //提升後:int? -(int? nullInt) Output: null
Console.WriteLine(-five); //提升後:int? -(int? five) Output: -5
Console.WriteLine(five + nullInt); //提升後:int? +(int? five, int? nullInt) Output: null
Console.WriteLine(five + five); //提升後:int? +(int? five, int? five) Output: 10
Console.WriteLine(nullInt == nullInt); //提升後:bool ==(int? nullInt, int? nullInt) Output: true
Console.WriteLine(five == five); //提升後:bool ==(int? five, int? five) Output: true
Console.WriteLine(five == nullInt); //提升後:bool ==(int? five, int? nullInt) Output: false
Console.WriteLine(five == four); //提升後:bool ==(int? five, int? four) Output: false
Console.WriteLine(four < five); //提升後:bool <(int? four, int? five) Output: false
Console.WriteLine(nullInt < five); //提升後:bool <(int? nullInt, int? five) Output: false
Console.WriteLine(five < nullInt); //提升後:bool <(int? five, int? nullInt) Output: false
Console.WriteLine(nullInt < nullInt); //提升後:bool <(int? nullInt, int? nullInt) Output: false
Console.WriteLine(nullInt <= nullInt); //提升後:bool <=(int? nullInt, int? nullInt) Output: false關於最後一個兩個空值的「小於等於」關係:雖然在相等測試(==)中,空值應該等於空值,但是不能認為一個空值「小於等於」另一個空值,因此為false
可空邏輯
真值表
假如bool?的結果取決於某變量的值,而該變量為null,結果必然為null
bool?的結果是true/false還是null,取決於具體的操作值
可空類型使用as操作符
對可空類型使用as操作符的結果:
空值(原始引用為錯誤類型 || HasValue == false)
有意義的值(Value)
空合并操作符(null coalescing)- ??
“??” 操作符
獲取兩個操作數,如果左邊操作數 != null,返回該操作數的值;否則返回右邊操作數的值
既可用於引用類型,也可用於值類型
設有兩個可空類型值first和second,進行first ?? second求值的過程大致為:
對first求值
如結果非空,返回first.Value
否則返回second(!second.HasValue ? null : second.Value)
假如second的類型是first的基礎類型(非可空)(int? first, int second)
??左側的項必須是可空類型;??右側的項可空 || 非可空
最終的結果類型為基礎類型(非可空)
int? a = 5;
int b = 10;
//int? c = a ?? b; 即使聲明為int?,c.GetType()後會發現類型依然為System.Int32
int c = a ?? b; //結果為int類型, 由於a != null,因此結果為 int c = 5;表達式從左到右求值,遇到第一個非空的值返回,如果全部為空則返回null
CLR對可空值類型的支持
GetType
在可空值類型上調用GetType
int? a = 10;
Console.WriteLine(a.GetType());會輸出可空實例內的值的類型
也就是會輸出Nullable<T>中的T,而不是Nullable<T>
調用接口方法
int? a = 10;
int? b = 30;
((IComparable)a).CompareTo(b);雖然Nullable<int>沒有和int一樣實現了IComparable的接口,但是仍然可以將Nullable<T>轉換成IComparable並通過編譯
可空類型的操作性能
雖然C#允許開發人員在可空實例上執行轉換、轉型和應用操作符
但是操作可空實例會生成大量代碼,運行速度也會低於非可空類型
可空實例
int? a = 10;
int? b = 30;
int? result = a + b;IL_0001: ldloca.s V_0
IL_0003: ldc.i4.s 10
IL_0005: call instance void valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
IL_000a: ldloca.s V_1
IL_000c: ldc.i4.s 30
IL_000e: call instance void valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
IL_0013: ldloc.0
IL_0014: stloc.s V_6
IL_0016: ldloc.1
IL_0017: stloc.s V_7
IL_0019: ldloca.s V_6
IL_001b: call instance bool valuetype [System.Runtime]System.Nullable`1<int32>::get_HasValue()
IL_0020: ldloca.s V_7
IL_0022: call instance bool valuetype [System.Runtime]System.Nullable`1<int32>::get_HasValue()
IL_0027: and
IL_0028: brtrue.s IL_0036
IL_002a: ldloca.s V_8
IL_002c: initobj valuetype [System.Runtime]System.Nullable`1<int32>
IL_0032: ldloc.s V_8
IL_0034: br.s IL_004a
IL_0036: ldloca.s V_6
IL_0038: call instance !0 valuetype [System.Runtime]System.Nullable`1<int32>::GetValueOrDefault()
IL_003d: ldloca.s V_7
IL_003f: call instance !0 valuetype [System.Runtime]System.Nullable`1<int32>::GetValueOrDefault()
IL_0044: add
IL_0045: newobj instance void valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
非可空實例
int c = 10;
int d = 30;
int result_2 = c + d;IL_0061: ldc.i4.s 10
IL_0063: stloc.3
IL_0064: ldc.i4.s 30
IL_0066: stloc.s V_4
IL_0068: ldloc.3
IL_0069: ldloc.s V_4
IL_006b: add
IL_006c: stloc.s V_5
IL_006e: ldstr "Result_2: {0}"
參考書目
- 《CLR via C#》(第4版) Jeffrey Richter
- 《深入理解C#》(第3版) Jon Skeet