.NET 类型转换工具
转换 System.Convert 无法转换的内容。
引言
虽然 System.Convert 类在类型转换方面非常有用,但正如一些人可能遇到的那样,它只能转换实现 IConvertible 接口的类型,即使你要转换到的类具有显式或隐式转换运算符。
随着 .NET 中泛型和动态类型的可用,程序员现在更多地处理未知类型。有时我们需要能够检查该类型是否可以转换为另一种类型,或者该类型是否支持为另一种类型定义的转换运算符。
此外,此工具还扩展了 default 关键字的功能。目前,default 只能创建两种值之一:0(如果类型是原始值类型)或 null(如果类型是引用类型)。 此处的工具将在类型具有默认构造函数时创建该类型的默认实例,否则返回 null。
背景
System.Convert
可以转换实现 IConvertible
的任何类型,但不支持未实现该接口的类型,即使该类型具有隐式或显式运算符可转换为其他 IConvertible
类型。这是因为 System.Convert
工具在尝试转换之前会首先检查 IConvertible
的实现,如果类型未实现该接口,则会抛出异常。
那么,当你在编译时处理未知类型并需要验证这些类型是否可以转换为另一种类型时会发生什么?你可以选择声明你正在处理的类型需要支持 IConvertible
(这仅支持原始类型),或者你可以尝试进行转换并捕获异常。
ConvertAny
工具通过提供以下方法来解决此问题:
CanConvert<ToType>(object)
- 测试对象是否可转换为 ToType。如果对象可转换,则返回 true,否则返回 false。
CanConvert(Type, Type)
- 测试 from 类型是否可转换为 to 类型。这仅适用于类型引用,不需要对象引用。
TryConvert<ToType>(object, out ToType)
- 尝试转换类型,非常类似于许多原始类型上找到的 TryParse 方法。如果转换失败,ToType 将是
default
(ToType)(系统默认值,不是工具的默认值)。 -
Convert<ToType>(object)
- 这会转换类型,如果对象无法转换为 ToType,则会抛出异常。
ConvertByType(object, Type)
- 这会使用
Type
引用来转换对象。如果对象无法转换为该类型,则会抛出异常。 Default<T>()
- 创建类型的默认实例。如果类型是原始类型,其行为与 default 关键字相同。如果类型具有默认构造函数,它将创建该类型的实例。如果类型没有默认构造函数,此方法将返回 null。
DefaultByType(Type)
- 与上面的方法相同,只是它使用
Type
引用而不是泛型。
类型检查/转换的工作原理
转换可能是一项昂贵的操作,因此按成本从低到高的顺序尝试最便宜的操作至关重要。这样可以加快对象易于转换为另一种类型的速度。
让我们来看看 CanConvert<ToType>(object)
代码:
public static bool CanConvert<ToType>(object value)
{
if (value == null)
throw new ArgumentNullException("value", StringResources.ArgumentNullException);
Type tType = typeof(ToType);
Type fType = value.GetType();
if (fType == tType)
return true;
if (typeof(IConvertible).IsAssignableFrom(tType) &&
typeof(IConvertible).IsAssignableFrom(fType))
{
return true;
}
try
{
//Casting a boxed object to a type even though the type supports an explicit cast to
//that type will fail. The only way to do this is to try to find the explicit or
//implicit type conversion operator on the to type that supports the from type.
MethodInfo mi = tType.GetMethods().FirstOrDefault(m =>
(m.Name == "op_Explicit" || m.Name == "op_Implicit") &&
m.ReturnType == tType &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == fType
);
if (mi == null)
{
//We can search for the reverse one on the original type too...
mi = fType.GetMethods().FirstOrDefault(m =>
(m.Name == "op_Explicit" || m.Name == "op_Implicit") &&
m.ReturnType == tType &&
m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType == fType
);
}
if (mi != null)
return true;
return false;
}
catch
{
return false;
}
}
此代码模式在其他函数中是相同的,仅针对返回值、boolean
或为 TryConvert
方法使用 out 参数进行修改。
如你所见,我们首先检查类型是否相同,如果相同,则返回 true。接下来,我们将检查类型是否都是 IConvertible,如果是,我们就知道 System.Convert 可以轻松转换它们,因此我们返回 true。
最后一部分是关键。如果你阅读 try 块中的注释,你会发现转换装箱对象可能会失败,即使这些对象定义了转换运算符。这让我沮丧了好几个小时,因为在 Watch 窗口中转换工作正常,而且我可以看到对象具有转换运算符,但它每次都会抛出异常。
为了解决这个问题,我们必须通过反射来调用运算符,而不是使用强制类型转换。写 (ToType)value
然后在 try 块中返回 true
,或者在 catch 块中返回 false
会很容易,但这样行不通。
由于转换运算符可能定义在任一类型上,因此我们需要检查这两种类型是否存在转换运算符。如果它在第一个类型上找不到,我们将检查第二个类型是否存在运算符。如果其中任何一个检查找到了运算符,那么它就可以工作。
新的默认值
default 关键字的实现让我恼火了一段时间。default 只有三种可能的选项:对于 int
、long
、double
、float
等原始类型,它返回 0;对于结构体(因为结构体不能为 null),它返回基本构造类型;对于引用类型,它返回 null
。
对于引用类型,如果类型包含默认构造函数,它返回该类型的默认实例会很方便。我在我的某些代码中需要这种行为,所以我编写了一个 Type 类的扩展来创建默认实例。
public static object CreateDefault(this Type t)
{
return ConvertAny.DefaultByType(t);
}
调用 ConvertAny
类内部的此函数。
public static T Default<T>()
{
if (typeof(T).IsPrimitive)
return default(T);
ConstructorInfo cInfo = typeof(T).GetConstructor(Type.EmptyTypes);
if (cInfo == null)
return default(T);
return (T)cInfo.Invoke(null);
}
public static object DefaultByType(Type type)
{
MethodInfo generic = _genericDefaultMethod.MakeGenericMethod(type);
return generic.Invoke(null, null);
}
_genericDefaultMethod
只是一个指向 Default<T>
函数的缓存引用,该函数在 ConvertAny
静态构造函数运行时创建。虽然很简单,但它做了一些有趣的事情。首先,它检查类型是否是原始类型,如果是,则只返回该类型的正常 default(T)
。接下来,它查找一个没有参数的构造函数(默认构造函数),如果找到一个,则返回该类型的默认实例。
使用代码
项目中包含的单元测试证明了该类的操作,它提供了许多使用各种函数的方法,因为它为 ConvertAny
和 TypeExtensions
类中定义的每个公共方法都包含了一个测试。 这里我们将仅介绍一些基本用法。
查找一个类型是否可以转换为另一个类型
要做到这一点,如果你有强类型引用,可以使用 CanConvert<ToType>(object)
函数;如果你只有值的 Type
对象引用,则可以使用 CanConvert(Type, Type)
函数。
ConvertAny.CanConvert<bool>(1); //Check if int 1 can be converted to bool
ConvertAny.CanConvert<TestCastibleClass>((double)1.0); //Check if double 1.0 can be converted to TestCastibleClass
ConvertAny.CanConvert(typeof(int), typeof(bool)); //Test if an int can be converted to a bool
ConvertAny.CanConvert(typeof(TestCastibleClass), typeof(double)); //Test if TestCastibleClass can be converted to double
尝试将一个类型转换为另一个类型
如果你想将转换和检查合并为一个调用(并提高性能),则可以使用 TryConvert<ToType>(object, out ToType)
方法。为什么库没有 TryConvertByType(object, Type, out object)
方法?我将其留给你们作为练习,要实现它,请参考 Convert 和 ConvertByType 方法。
bool b = false;
ConvertAny.TryConvert<bool>(1, out b);
TestCastibleClass tc = null;
ConvertAny.TryConvert<TestCastibleClass>(15.2, out tc);
转换一个值
如果你想跳过检查直接转换值,可以使用 Convert<ToType>(object)
或 ConvertByType(object, Type) 方法。但请注意,如果它们无法将对象转换为所需的类型,它们将抛出 InvalidCastException。
ConvertAny.Convert<bool>(0);
ConvertAny.Convert<TestCastibleClass>(12.2);
ConvertAny.ConvertByType(1, typeof(bool));
ConvertAny.ConvertByType(new TestCastibleClass(), typeof(double));
使用类型扩展
有两个类型扩展,如前所述:第一个是获取类型的默认实例 CreateDefault
,第二个是检查类型是否可转换为另一个类型 CanConvertTo(Type)
。
object o = typeof(TestCastibleClass).CreateDefault();
typeof(int).CanConvertTo(typeof(double));
typeof(TestCastibleClass).CanConvertTo(typeof(double));
关注点
既然 .NET 在 4.0 版本中引入了 dynamic
运行时类型,为什么还要费力去做这些呢?
最初,我设计这个类是为了将任何 dynamic
类型转换为任何其他类型。老实说,使用 dynamic
关键字实现这个类比使用 object
要简单得多,装箱类型的类型转换有效,转换也很简单。
然后我设计了我的单元测试(是的,我知道,测试应该先完成),当我运行它们时,我很震惊。一些转换例程需要 100 毫秒才能运行。我不得不离开我的电脑大约 30 分钟,当我坐下来研究它时,我发现了问题。动态类型在 .NET 中非常快,如果它们可以被缓存。当 DLR 遇到动态类型时,它会将站点添加到查找表中,这样下次就不必构建类型信息。它不能在用动态参数调用的静态类中做到这一点,因为参数会改变。
这导致了巨大的性能损失,因为我最初将这个类应用于一个脚本库,该库不能处理静态类型对象,并且每次运行可能包含数百次检查/转换。当我重写 ConvertAny
工具使用对象和反射时,性能提高了大约 100 倍(现在运行特定测试需要 1 毫秒或更少)。
这里的教训是,虽然 dynamic
可能会让你的生活更轻松,但你在使用它时必须意识到性能方面的影响。彻底测试(即使在你编写代码之后),并确保你的性能与应用程序的要求相匹配。
历史
2014/1/13 - 初始版本
2014/1/14 - 添加了空引用检查,并减少了函数中 typeof
的使用,感谢 Oliver Albrecht。