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

posted @ 2012-12-01 00:22  CYJB  阅读(9309)  评论(0编辑  收藏  举报
Fork me on GitHub