int? 竟然真的可以是 null!.NET/C# 確定可空值類型 Nullable 實例的真實類型

  • 2020 年 2 月 10 日
  • 筆記

使用 Nullable<T> 我們可以為原本不可能為 null 的值類型像引用類型那樣提供一個 null 值。不過注意:Nullable<T> 本身也是個 struct,是個值類型哦。這意味著你隨時可以調用 .HasValue 這樣的方法,而不用擔心會出現 NullReferenceException

等等!除了本文提到的一些情況。

Nullable 中的 null

注意看以下的程式碼。我們創建了一個值為 nullint?,然後依次輸出 value 的值、value.GetType()

你覺得可以得到什麼結果呢?

public class Program  {      public static void Main(string[] args)      {          int? value = GetValue(null);            Console.WriteLine($"value = {value}");          Console.WriteLine($"type  = {value.GetType()}");          Console.WriteLine($"TYPE  = {typeof(int?)}");            Console.ReadLine();      }        private static int? GetValue(int? source) => source;  }

結果是……

果是……

是……

……

崩掉了……

那麼我們在 value 後面加個空傳遞運算符:

--  Console.WriteLine($"type  = {value.GetType()}");  ++  Console.WriteLine($"type  = {value?.GetType()}");

現在再次運行,我們確認了 value?.GetType() 的值為 null;而 typeof(int?) 的類型為 Nullable<Int32>

然而,我們現在將 value 的值從 null 改為 1

--  int? value = GetValue(null);  ++  int? value = GetValue(1);

竟然 value.GetType() 得到的類型是 Int32

於是我們可以得出結論:

  1. 對於可空值類型,當為 null 時,GetType() 會出現空引用異常;
  2. 對於可空值類型,當不為 null 時,GetType() 返回的是對應的基礎類型,而不是可空值類型;
  3. typeof(int?) 能夠得到可空值類型。

Object.GetType() 和 is 對 Nullable 的作用

docs.microsoft.com 中,有一段對此的描述:

When you call the Object.GetType method on an instance of a nullable type, the instance is boxed to Object. As boxing of a non-null instance of a nullable type is equivalent to boxing of a value of the underlying type, GetType returns a Type object that represents the underlying type of a nullable type.

意思是說,當你對一個可空值類型 Nullable<T> 調用 Object.GetType() 方法的時候,這個實例會被裝箱,會被隱式轉換為一個 object 對象。然而對可空值類型的裝箱與對值類型本身的裝箱是同樣的操作,所以調用 GetType() 的時候都是返回這個對象對應的實際基礎類型。例如對一個 int? 進行裝箱和對 int 裝箱得到的 object 對象是一樣的,於是 GetType() 實際上是不能區分這兩種情況的。

那什麼樣的裝箱會使得兩個不同的類型被裝箱為同一個了呢?

另一篇文檔描述了 Nullable<T> 裝箱的過程:

  • If HasValue returns false, the null reference is produced.
  • If HasValue returns true, a value of the underlying value type T is boxed, not the instance of Nullable.
  • 如果 HasValue 返回 false,那麼就裝箱一個 null
  • 如果 HasValue 返回 true,那麼就將 Nullable<T> 中的 T 進行裝箱,而不是 Nullable<T> 的實例。

這才是為什麼 GetType() 會得到以上結果的原因。

同樣的,也不能使用 is 運算符來確定這個類型到底是不是可空值類型:

Console.WriteLine($"value is int  = {value is int}");  Console.WriteLine($"value is int? = {value is int?}");

最終得到兩者都是 True

應該如何判斷可空值類型的真實類型

使用 Nullable.GetUnderlyingType(type) 方法,能夠得到一個可空值類型中的基礎類型,也就是得到 Nullable<T>T 的類型。如果得不到就返回 null

所以使用以下方法可以判斷 type 的真實類型。

bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;

然而,這個 type 的實例怎麼來呢?根據前面的示例程式碼,我們又不能調用 GetType() 方法。

實際上,這個 type 的實例就是拿不到,在運行時是不能確定的。我們只能在編譯時確定,就像下面這樣:

bool IsOfNullableType<T>(T _) => Nullable.GetUnderlyingType(typeof(T)) != null;

如果你是運行時拿到的可空值類型的實例,那麼實際上此方法也是無能為力的。

public class Program  {      public static void Main(string[] args)      {          Console.Title = "walterlv's demo";            int? value = GetValue(1);          object o = value;          Console.WriteLine($"value is nullable? {IsOfNullableType(value)}");          Console.WriteLine($"o     is nullable? {IsOfNullableType(o)}");            Console.ReadLine();      }        private static int? GetValue(int? source) => source;        static bool IsOfNullableType<T>(T _) => Nullable.GetUnderlyingType(typeof(T)) != null;  }
參考資料

本文會經常更新,請閱讀原文: https://blog.walterlv.com/post/how-to-identify-a-nullable-type.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含鏈接: https://blog.walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發布。如有任何疑問,請 與我聯繫 ([email protected])