C# 判斷類型間能否隱式或強制類型轉換,以及開放泛型類型轉換 update 2015.02.03


如果要判斷某個實例是否與其它類型兼容,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

一、隱式類型轉換

首先對隱式類型轉換進行判斷,隱式類型轉換時,需要保證不會報錯,數據不會有丟失。我目前總結出下面五點:

  1. 如果要轉換到 object 類型,那么總是可以進行隱式類型轉換,因為 object 類型是所有類型的基類。
  2. 兼容的類型總是可以進行隱式類型轉換,例如子類轉換為父類。這個是顯然的,而且可以直接通過 Type.IsAssignableFrom 來判斷。
  3. .NET 的一些內置類型間總是可以進行隱式類型轉換,例如 int 可以隱式轉換為 long,這個稍后會總結為一個表格。
  4. 存在隱式類型轉換運算符的類型,這個是屬於用戶自定義的隱式類型轉換方法,可以通過 Type.GetMethods 查找名為 op_Implicit 的方法來找到所有的自定義隱式類型轉換。
  5. 除了上面的之外,.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 類型的用戶定義的隱式轉換按下面這樣處理:

  1. 確定類型 S0 和 T0。如果 S 或 T 是可以為 null 的類型,則 S0 和 T0 為它們的基礎類型;否則 S0 和 T0 分別等於 S 和 T。這里這樣做僅僅是為了在下一步查找用戶自定義的隱式轉換運算符,因為 Nullalbe<T> 類型顯然並不包含 T 中定義的運算符。
  2. 查找類型集 D,將從該類型集考慮用戶定義的轉換運算符。此集由 S0(如果 S0 是類或結構)、S0 的所有基類(如果 S0 是類)和 T0(如果 T0 是類或結構)組成。這里包含 S0 的某個基類,是因為 S 也可以使用基類中定義的轉換到其他類的類型轉換運算符。
  3. 查找適用的用戶定義轉換運算符和提升轉換運算符集 U。此集合由用戶定義的隱式轉換運算符和提升隱式轉換運算符組成,這些運算符是在 D 中的類或結構內聲明的,用於從包含 S 的類型(即 S 或 S 的基類和實現的接口)轉換為被 T 包含的類型(即 T 或 T 的子類)。如果 U 為空,則轉換未定義並且發生編譯時錯誤。
  4. 在 U 中查找運算符的最精確的源類型 SX
    • 如果 U 中存在某一運算符從 S 轉換,則 SX 為 S。
    • 否則,SX 是在 U 的運算符源類型的集合中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則轉換是不明確的,並且發生編譯時錯誤。這里比較難理解,簡單來說 SX 實際就是在繼承鏈中最“接近” S 的類,S 的基類就比 S 的基類的基類要更接近 S。
  5. 在 U 中查找運算符的最精確的目標類型 TX
    • 如果 U 中存在某一運算符轉換為 T,則 TX 為 T。
    • 否則,TX 是 U 的運算符源類型的集合中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則轉換是不明確的,並且發生編譯時錯誤。這里的 TX 是在繼承鏈中最“接近” T 的類,這里與 S 不同的是,需要注意 Tx 總是 T 或 T 的子類。
  6. 查找最具體的轉換運算符:
    • 如果 U 中只含有一個從 SX 轉換到 TX 的用戶定義轉換運算符,則這就是最精確的轉換運算符。
    • 否則,如果 U 恰好包含一個從 SX 轉換到 TX 的提升轉換運算符,則這就是最具體的轉換運算符。
    • 否則,轉換是不明確的,並發生編譯時錯誤。
  7. 最后,應用轉換:
    • 如果 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; }
}

二、強制類型轉換

在進行強制類型轉換時,無需保證數據不丟失,也無需保證類型一定能夠兼容。判斷的過程也類似於隱式類型轉換,同樣總結為五點:

  1. 總是可以與 object 類型進行相互轉換。
  2. 兼容的類型不但可以從子類轉換為父類,也可以從父類轉換為子類。
  3. .NET 的一些內置類型間總是可以進行強制類型轉換,這是另外一個表格。
  4. 存在隱式或顯式類型轉換運算符的類型,可以通過 Type.GetMethods 查找名為 op_Explicit 的方法來找到所有的自定義顯式類型轉換。
  5. 仍然要考慮 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM