如果要判斷某個實例是否與其它類型兼容,C# 已經提供了兩個運算符 is 和 as,Type 類也提供了 IsAssignableFrom 方法來對兩個類型進行判斷。但他們實際上判斷的是類型是否在繼承層次結構中,而不是類型間是否可以進行轉換。例如下面的代碼:
long a = 0; Console.WriteLine(a is int); Console.WriteLine(typeof(long).IsAssignableFrom(typeof(int))); Console.WriteLine(typeof(int).IsAssignableFrom(typeof(long)));
它們的返回值都是 False,但實際上 int 類型可以隱式轉換為 long 類型的,long 類型也可以強制轉換為 int 類型。如果希望知道一個類型能否隱式或強制轉換為其它類型,這些已有的方法就力不能及了,只能自己來進行判斷。
2015.02.03 更新:我利用 IL 實現了一套完整的運行時類型轉換框架,因此這里前兩節所述的實現方式已被廢棄,新的實現效率更高,功能更完善。更多信息請參考《使用 IL 實現類型轉換》,代碼實現可見 Cyjb.Conversions 和 Cyjb.Reflection。
一、隱式類型轉換
首先對隱式類型轉換進行判斷,隱式類型轉換時,需要保證不會報錯,數據不會有丟失。我目前總結出下面五點:
- 如果要轉換到 object 類型,那么總是可以進行隱式類型轉換,因為 object 類型是所有類型的基類。
- 兼容的類型總是可以進行隱式類型轉換,例如子類轉換為父類。這個是顯然的,而且可以直接通過 Type.IsAssignableFrom 來判斷。
- .NET 的一些內置類型間總是可以進行隱式類型轉換,例如 int 可以隱式轉換為 long,這個稍后會總結為一個表格。
- 存在隱式類型轉換運算符的類型,這個是屬於用戶自定義的隱式類型轉換方法,可以通過 Type.GetMethods 查找名為 op_Implicit 的方法來找到所有的自定義隱式類型轉換。
- 除了上面的之外,.NET 里還有一個略微特殊的類型:Nullable<T>,T 類型總是可以隱式轉換為 Nullable<T>,而且 Nullable<T1> 和 Nullable<T2> 類型之間的轉換,需要看 T1 和 T2 能否進行隱式轉換。因此這里需要特殊處理下,才能夠正確的進行判斷。
.Net 內置類型間的隱式轉換,我總結成了下面的表格(未包含自身的轉換):
要轉換到的類型 | 來源類型 |
Int16 | SByte, Byte |
UInt16 | Char, Byte |
Int32 | Char, SByte, Byte, Int16, UInt16 |
UInt32 | Char, Byte, UInt16 |
Int64 | Char, SByte, Byte, Int16, UInt16, Int32, UInt32 |
UInt64 | Char, Byte, UInt16, UInt32 |
Single | Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64 |
Double | Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single |
Decimal | Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64 |
對於類型的自定義隱式類型轉換,則有些需要特別注意的地方。
自定義隱式類型轉換可以有兩個方向:轉換自和轉換到,區別就是參數和返回值不同。例如,下面的方法表示 Test 類型可以隱式轉換自 int 類型:
public static implicit operator Test(int t);
而下面的方法則表示 Test 類型可以隱式轉換到 int 類型:
public static implicit operator int(Test t);
因此,在判斷類型間是否可以隱式類型轉換時,需要考慮這兩種不同的情況,而且同時還可能存在兼容類型轉換或內置類型轉換。但是,卻不可以進行兩次隱式類型轉換。
也就是說,假設 Test 類型可以隱式轉換為 int 類型或 Test3 類型,Test3 類繼承自 Test2 類,Test3 類可以隱式轉換為 Test4 類,圖 1 的綠色隱式類型轉換是合法的,紅色是非法的:
圖 1 合法和非法的隱式類型轉換
這里的判斷算法如圖 2 所示,設要判斷 fromType 能否隱式轉換為 type 類型,取集合 S1 為 type 的隱式轉換自方法集合,S2 為 fromType 的隱式轉換到方法集合,那么分別判斷 S1 中是否存在與 fromType 類型兼容或者可以進行內置類型轉換的類型,和 S2 中是否存在與 type 類型兼容或者可以進行內置類型轉換的類型。如果找到了這樣的類型,就表示可以成功進行隱式類型轉換。
圖 2 隱式類型轉換判斷算法
判斷算法的核心代碼如下:
/// <summary> /// 確定當前的 <see cref="System.Type"/> 的實例是否可以從指定 <see cref="System.Type"/> /// 的實例進行隱式類型轉換。 /// </summary> /// <param name="type">要判斷的實例。</param> /// <param name="fromType">要與當前類型進行比較的類型。</param> /// <returns>如果當前 <see cref="System.Type"/> 可以從 <paramref name="fromType"/> /// 的實例分配或進行隱式類型轉換,則為 <c>true</c>;否則為 <c>false</c>。</returns> public static bool IsImplicitFrom(this Type type, Type fromType) { if (type == null || fromType == null) { return false; } // 總是可以隱式類型轉換為 Object。 if (type == typeof(object)) { return true; } // 對 Nullable<T> 的支持。 Type[] genericArguments; if (InInheritanceChain(typeof(Nullable<>), type, out genericArguments)) { type = genericArguments[0]; if (InInheritanceChain(typeof(Nullable<>), fromType, out genericArguments)) { fromType = genericArguments[0]; } } // 判斷是否可以從實例分配。 if (IsAssignableFromEx(type, fromType)) { return true; } // 對隱式類型轉換運算符進行判斷。 if (GetTypeOperators(type).Any(pair => pair.Value.HasFlag(OperatorType.ImplicitFrom) && IsAssignableFromEx(pair.Key, fromType))) { return true; } if (GetTypeOperators(fromType).Any(pair => pair.Value.HasFlag(OperatorType.ImplicitTo) && IsAssignableFromEx(type, pair.Key))) { return true; } return false; }
2013.3.17 更新:后來我又研究了《CSharp Language Specification》v5.0 中與隱式類型轉換相關的部分,發現上面的圖 2 並不完全准確,根據 6.1.11 用戶定義的隱式轉換 這一節的說明,用戶定義的隱式轉換由以下三部分組成:先是一個標准的隱式轉換(可選);然后是執行用戶定義的隱式轉換運算符;最后是另一個標准的隱式轉換(可選)。
圖 3 規范中的隱式類型轉換判斷算法
所以,用戶定義的隱式轉換比我之前考慮的要更復雜些(自己總結的難免會有疏漏,看來還是要根據規范才行),而“標准隱式轉換”則包括下面這些轉換:
- 標識轉換(參見第 6.1.1 節,簡單的說就是相同類型間可以進行隱式轉換。)
- 隱式數值轉換(參見第 6.1.2 節,就是我上面總結的 .Net 內置類型間的隱式轉換。)
- 可以為 null 的隱式轉換(參見第 6.1.4 節,就是我上面總結的第 5 點相同。)
- 隱式引用轉換(參見第 6.1.6 節,基本上就是 Type.IsAssignableFrom 的功能。)
- 裝箱轉換(參見第 6.1.7 節,同樣包含在 Type.IsAssignableFrom 的功能中,就是特別針對值類型進行了說明。)
- 隱式常量表達式轉換(參見第 6.1.8 節,針對編譯時可以確定值得常量表達式,運行時無需考慮。)
- 涉及類型形參的隱式轉換(參見第 6.1.10 節,針對類型參數 T 的轉換,同樣無需考慮。)
下面給出在 6.4.4 用戶定義的隱式轉換 節定義的算法,這個算法中文版的翻譯有問題,我根據英文版重新修改了一些地方,並加入了自己的說明:
從 S 類型到 T 類型的用戶定義的隱式轉換按下面這樣處理:
- 確定類型 S0 和 T0。如果 S 或 T 是可以為 null 的類型,則 S0 和 T0 為它們的基礎類型;否則 S0 和 T0 分別等於 S 和 T。這里這樣做僅僅是為了在下一步查找用戶自定義的隱式轉換運算符,因為 Nullalbe<T> 類型顯然並不包含 T 中定義的運算符。
- 查找類型集 D,將從該類型集考慮用戶定義的轉換運算符。此集由 S0(如果 S0 是類或結構)、S0 的所有基類(如果 S0 是類)和 T0(如果 T0 是類或結構)組成。這里包含 S0 的某個基類,是因為 S 也可以使用基類中定義的轉換到其他類的類型轉換運算符。
- 查找適用的用戶定義轉換運算符和提升轉換運算符集 U。此集合由用戶定義的隱式轉換運算符和提升隱式轉換運算符組成,這些運算符是在 D 中的類或結構內聲明的,用於從包含 S 的類型(即 S 或 S 的基類和實現的接口)轉換為被 T 包含的類型(即 T 或 T 的子類)。如果 U 為空,則轉換未定義並且發生編譯時錯誤。
- 在 U 中查找運算符的最精確的源類型 SX:
- 如果 U 中存在某一運算符從 S 轉換,則 SX 為 S。
- 否則,SX 是在 U 的運算符源類型的集合中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則轉換是不明確的,並且發生編譯時錯誤。這里比較難理解,簡單來說 SX 實際就是在繼承鏈中最“接近” S 的類,S 的基類就比 S 的基類的基類要更接近 S。
- 在 U 中查找運算符的最精確的目標類型 TX:
- 如果 U 中存在某一運算符轉換為 T,則 TX 為 T。
- 否則,TX 是 U 的運算符源類型的集合中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則轉換是不明確的,並且發生編譯時錯誤。這里的 TX 是在繼承鏈中最“接近” T 的類,這里與 S 不同的是,需要注意 Tx 總是 T 或 T 的子類。
- 查找最具體的轉換運算符:
- 如果 U 中只含有一個從 SX 轉換到 TX 的用戶定義轉換運算符,則這就是最精確的轉換運算符。
- 否則,如果 U 恰好包含一個從 SX 轉換到 TX 的提升轉換運算符,則這就是最具體的轉換運算符。
- 否則,轉換是不明確的,並發生編譯時錯誤。
- 最后,應用轉換:
- 如果 S 不是 SX,則執行從 S 到 SX 的標准隱式轉換。
- 調用最具體的轉換運算符,以從 SX 轉換到 TX。
- 如果 TX 不是 T,則執行從 TX 到 T 的標准隱式轉換。
下面就是更改后的核心代碼:
public static bool IsImplicitFrom(this Type type, Type fromType) { if (type == null || fromType == null) { return false; } // 對引用類型的支持。 if (type.IsByRef) { type = type.GetElementType(); } if (fromType.IsByRef) { fromType = type.GetElementType(); } // 總是可以隱式類型轉換為 Object。 if (type.Equals(typeof(object))) { return true; } // 判斷是否可以進行標准隱式轉換。 if (IsStandardImplicitFrom(type, fromType)) { return true; } // 對隱式類型轉換運算符進行判斷。 // 處理提升轉換運算符。 Type nonNullalbeType, nonNullableFromType; if (IsNullableType(type, out nonNullalbeType) && IsNullableType(fromType, out nonNullableFromType)) { type = nonNullalbeType; fromType = nonNullableFromType; } return ConversionCache.GetImplicitConversion(fromType, type) != null; } internal static bool IsStandardImplicitFrom(this Type type, Type fromType) { // 對 Nullable<T> 的支持。 if (!type.IsValueType || IsNullableType(ref type)) { fromType = GetNonNullableType(fromType); } // 判斷隱式數值轉換。 HashSet<TypeCode> typeSet; // 這里加入 IsEnum 的判斷,是因為枚舉的 TypeCode 是其基類型的 TypeCode,會導致判斷失誤。 if (!type.IsEnum && ImplicitNumericConversions.TryGetValue(Type.GetTypeCode(type), out typeSet)) { if (!fromType.IsEnum && typeSet.Contains(Type.GetTypeCode(fromType))) { return true; } } // 判斷隱式引用轉換和裝箱轉換。 return type.IsAssignableFrom(fromType); }
下面附上一些隱式類型轉換判斷方法(IsImplicitFrom)和 Type.IsAssignableFrom 方法的對比:
Console.WriteLine(typeof(object).IsAssignableFrom(typeof(uint))); // True Console.WriteLine(typeof(object).IsImplicitFrom(typeof(uint))); // True Console.WriteLine(typeof(int).IsAssignableFrom(typeof(short))); // False Console.WriteLine(typeof(int).IsImplicitFrom(typeof(short))); // True Console.WriteLine(typeof(long?).IsAssignableFrom(typeof(int?))); // False Console.WriteLine(typeof(long?).IsImplicitFrom(typeof(int?))); // True Console.WriteLine(typeof(long).IsAssignableFrom(typeof(TestClass))); // False Console.WriteLine(typeof(long).IsImplicitFrom(typeof(TestClass))); // True class TestClass { public static implicit operator int(TestClass t) { return 1; } }
二、強制類型轉換
在進行強制類型轉換時,無需保證數據不丟失,也無需保證類型一定能夠兼容。判斷的過程也類似於隱式類型轉換,同樣總結為五點:
- 總是可以與 object 類型進行相互轉換。
- 兼容的類型不但可以從子類轉換為父類,也可以從父類轉換為子類。
- .NET 的一些內置類型間總是可以進行強制類型轉換,這是另外一個表格。
- 存在隱式或顯式類型轉換運算符的類型,可以通過 Type.GetMethods 查找名為 op_Explicit 的方法來找到所有的自定義顯式類型轉換。
- 仍然要考慮 Nullable<T> 類型,不過在強制類型轉換時,T 類型總是可以與 Nullable<T> 相互轉換。
.Net 內置的強制類型轉換比較簡單,就是 Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double 和 Decimal 之間的相互轉換。
強制類型轉換的判斷算法也是與隱式類型轉換類似的,只不過是要考慮一下自定義的顯式類型轉換方法。
/// <summary> /// 確定當前的 <see cref="System.Type"/> 的實例是否可以從指定 <see cref="System.Type"/> /// 的實例進行強制類型轉換。 /// </summary> /// <param name="type">要判斷的實例。</param> /// <param name="fromType">要與當前類型進行比較的類型。</param> /// <returns>如果當前 <see cref="System.Type"/> 可以從 <paramref name="fromType"/> /// 的實例分配或進行強制類型轉換,則為 <c>true</c>;否則為 <c>false</c>。</returns> public static bool IsCastableFrom(this Type type, Type fromType) { if (type == null || fromType == null) { return false; } // 總是可以與 Object 進行強制類型轉換。 if (type == typeof(object) || fromType == typeof(object)) { return true; } // 對 Nullable<T> 的支持。 Type[] genericArguments; if (InInheritanceChain(typeof(Nullable<>), type, out genericArguments)) { type = genericArguments[0]; } if (InInheritanceChain(typeof(Nullable<>), fromType, out genericArguments)) { fromType = genericArguments[0]; } // 判斷是否可以從實例分配,強制類型轉換允許沿着繼承鏈反向轉換。 if (IsAssignableFromCastEx(type, fromType) || IsAssignableFromCastEx(fromType, type)) { return true; } // 對強制類型轉換運算符進行判斷。 if (GetTypeOperators(type).Any(pair => pair.Value.AnyFlag(OperatorType.From) && IsAssignableFromCastEx(pair.Key, fromType))) { return true; } if (GetTypeOperators(fromType).Any(pair => pair.Value.AnyFlag(OperatorType.To) && IsAssignableFromCastEx(type, pair.Key))) { return true; } return false; }
2012.12.27 更新:在強制類型轉換時,枚舉類型也是需要特殊處理的,處理方法也很簡單,就是將枚舉類型轉換為對應的基類型,再進行判斷。
if (type.IsEnum) { type = Enum.GetUnderlyingType(type); }
2012.12.31 更新:在對枚舉類型能否進行強制類型轉換進行判斷時,上面的方法是有問題的,僅僅簡單考慮了基礎類型,而沒有有效利用枚舉類型本身。實際上,僅枚舉類型及其基礎類型間可以進行強制類型轉換,或者是兩個枚舉類型之間進行強制類型轉換,其他情況則不需要考慮枚舉的基礎類型的問題。對應的代碼如下:
if (type.IsEnum) { if (fromType.IsEnum || IsEnumUnderlyingType(fromType)) { return true; } } else if (fromType.IsEnum && IsEnumUnderlyingType(type)) { return true; }
其中,IsEnumUnderlyingType 用於判斷類型是否是 Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64 之中的一個。
2013.3.17 更新:對於顯式轉換,根據《CSharp Language Specification》v5.0 中 6.2 節的說明,我上面總結的規則顯然是不全面的,顯式引用轉換中的一大部分都沒有實現(例如允許從任何 interface-type S 到任何 interface-type T)。不過,我之前沒有實現的部分並不屬於常用的顯式轉換,而且實現起來會很麻煩,尤其是涉及到泛型類的時候(有興趣的可以自己找找規范看看,6.2.4 節總共有 14 條,我僅實現了其中的 4 條),所以最后也沒有完整實現。
我基本改寫了一下之前的顯式類型轉換算法,用戶定義的顯式轉換部分也並未完全按照 6.4.5 節 用戶定義的顯式轉換 中說明的實現(有點復雜,而且效率不高),而是采用了類似用戶定義的隱式轉換的算法(反正實現本來就是不完整的,還是簡便起見的好)。
下面給出更改后的核心代碼:
public static bool IsExplicitFrom(this Type type, Type fromType) { if (type == null || fromType == null) { return false; } // 對引用類型的支持。 if (type.IsByRef) { type = type.GetElementType(); } if (fromType.IsByRef) { fromType = type.GetElementType(); } // 總是可以與 Object 進行顯示類型轉換。 if (type.Equals(typeof(object)) || fromType.Equals(typeof(object))) { return true; } // 對 Nullable<T> 的支持。 IsNullableType(ref type); IsNullableType(ref fromType); // 顯式枚舉轉換。 if (type.IsEnumExplicitFrom(fromType)) { return true; } // 判斷是否可以進行標准顯式轉換。 if (IsStandardExplicitFrom(type, fromType)) { return true; } // 對顯式類型轉換運算符進行判斷。 return ConversionCache.GetExplicitConversion(fromType, type) != null; } internal static bool IsStandardExplicitFrom(this Type type, Type fromType) { // 判斷顯式數值轉換。 // 這里加入 IsEnum 的判斷,是因為枚舉的 TypeCode 是其基類型的 TypeCode,會導致判斷失誤。 if (!type.IsEnum && ExplicitNumericConversions.Contains(Type.GetTypeCode(type)) && !fromType.IsEnum && ExplicitNumericConversions.Contains(Type.GetTypeCode(fromType))) { return true; } // 判斷正向和反向的隱式引用轉換和裝箱轉換。 return (type.IsAssignableFrom(fromType) || fromType.IsAssignableFrom(type)); } internal static bool IsEnumExplicitFrom(this Type type, Type fromType) { if (type.IsEnum) { if (fromType.IsEnum || ExplicitNumericConversions.Contains(Type.GetTypeCode(fromType))) { return true; } } else if (fromType.IsEnum && ExplicitNumericConversions.Contains(Type.GetTypeCode(type))) { return true; } return false; }
三、是否可以分配到開放泛型類型
開放泛型類型指的就是類似於 List<> 這樣的沒有提供類型參數的泛型類型,有時也是需要判斷
typeof(ICollection<>).IsAssignableFrom(typeof(List<int>))
的,不過很可惜,Type.IsAssignableFrom 方法同樣不適用,這里同樣需要自己來處理。
這里需要考慮的情況簡單些,假設 type 是要測試的開放泛型類型,fromType 是源類型,則有下面幾種情況:
fromType 是接口 | fromType 是類型 | |
type 是接口 | 判斷 fromType 及其實現的接口是否匹配 type | 判斷 fromType 實現的接口是否匹配 type |
type 是類 | 不可能匹配 | 判斷 fromType 的所有基類是否匹配 type |
而泛型類型間是否匹配的判斷,只要從 fromType 從將類型參數提取出來,再通過 type.MakeGenericType 生成相應的類型,就可以用 IsAssignableFrom 判斷類型是否匹配了,這樣做還可以一並獲取匹配的類型參數。
主要代碼為:
/// <summary> /// 確定當前的開放泛型類型的實例是否可以從指定 <see cref="System.Type"/> 的實例分配,並返回泛型的參數。 /// </summary> /// <param name="type">要判斷的開放泛型類型。</param> /// <param name="fromType">要與當前類型進行比較的類型。</param> /// <param name="genericArguments">如果可以分配到開放泛型類型,則返回泛型類型參數;否則返回 <c>null</c>。</param> /// <returns>如果當前的開放泛型類型可以從 <paramref name="fromType"/> 的實例分配, /// 則為 <c>true</c>;否則為 <c>false</c>。</returns> public static bool OpenGenericIsAssignableFrom(this Type type, Type fromType, out Type[] genericArguments) { if (type != null && fromType != null && type.IsGenericType) { if (type.IsInterface == fromType.IsInterface) { if (InInheritanceChain(type, fromType, out genericArguments)) { return true; } } if (type.IsInterface) { // 查找實現的接口。 Type[] interfaces = fromType.GetInterfaces(); for (int i = 0; i < interfaces.Length; i++) { if (InInheritanceChain(type, interfaces[i], out genericArguments)) { return true; } } } } genericArguments = null; return false; } /// <summary> /// 確定當前的開放泛型類型是否在指定 <see cref="System.Type"/> 類型的繼承鏈中,並返回泛型的參數。 /// </summary> /// <param name="type">要判斷的開放泛型類型。</param> /// <param name="fromType">要與當前類型進行比較的類型。</param> /// <param name="genericArguments">如果在繼承鏈中,則返回泛型類型參數;否則返回 <c>null</c>。</param> /// <returns>如果當前的開放泛型類型在 <paramref name="fromType"/> 的繼承鏈中, /// 則為 <c>true</c>;否則為 <c>false</c>。</returns> private static bool InInheritanceChain(Type type, Type fromType, out Type[] genericArguments) { // 沿着 fromType 的繼承鏈向上查找。 while (fromType != null) { if (fromType.IsGenericType) { genericArguments = fromType.GetGenericArguments(); if (genericArguments.Length == type.GetGenericArguments().Length) { try { Type closedType = type.MakeGenericType(genericArguments); if (closedType.IsAssignableFrom(fromType)) { return true; } } catch (ArgumentException) { // 不滿足參數的約束。 } } } fromType = fromType.BaseType; } genericArguments = null; return false; }
下面是一個例子:
typeof(IEnumerable<>).OpenGenericIsAssignableFrom(typeof(Dictionary<string, int>), out genericArguments);
方法可以正確的判斷出 Dictionary<string,int> 類型是可以分配到 IEnumerable<> 類型的,而且類型參數為 KeyValuePair<string,int>。
完整的代碼源文件可見 https://github.com/CYJB/.../TypeExt.cs。