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。
一、隐式类型转换
首先对隐式类型转换进行判断,隐式类型转换时,需要保证不会报错,数据不会有丢失。我目前总结出下面五点:
- 如果要转换到 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。
作者:CYJB
出处:http://www.cnblogs.com/cyjb/
GitHub:https://github.com/CYJB/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。