目录
2. ConvertToStringRepresentation()
最新发行说明!
只是为了让您知道:版本2.6 支持新的框架类型DateOnly和TimeOnly——并添加了更多日期时间格式。NuGet包也已更新。查看支持的转换。
注意
这篇文章不知何故已经过时了——通用类型转换器有一个全新的重写。现在,请查看新的项目站点。但是您仍然可以继续阅读——大多数解释的概念也适用于新版本。
介绍
本文介绍.NET Framework提供的类型转换的不同可能性。最后,它在UniversalTypeConverter中提供了所有这些方法(以及更多)的组合,其中几乎可以将每种类型转换为另一种类型。
在需要将来自数据库的值映射到某些对象时,我遇到了这个问题。当然,这是一个可以通过 O/R映射器完成的问题。但这一次,针对数据库的查询——以及要将结果映射到的对象——仅在运行时已知。这是一种报告工具。由于不知道确切的类型,我决定寻找通用转换解决方案。但是这些都没有提供像将所有所需类型将x转换为y这样的现成稳定方法。于是,这个UniversalTypeConverter想法诞生了。
只是为了让您了解我正在寻找的内容:
int myInt = myGivenValue.ConvertTo<int>();
无论myGivenValue的类型如何。
如果您继续阅读,您将深入了解转化。不仅从方案的角度来看——还描述了实际背景和原因。所以给你一杯咖啡,茶,啤酒或任何你喜欢的东西,然后继续阅读。
如果你对整个故事不感兴趣,你可以跳到最后,查看使用代码。
心中的解决方案
在我的研究过程中,我遇到了一些关于TypeConverter的好文章,我们很快就会仔细研究。在此之前,您可以想象TypeConverter是一种通用方法,用于告诉类型如何从/转换为另一种类型。这似乎是一个非常优雅的解决方案。所以我尝试了一些不同的类型——都工作正常——并认为我已经准备好做这项工作了。多么天真...
设置项目
起初,我建立了一个包含各种类型转换组合的矩阵,这对我来说似乎很有用。您可以查看此矩阵——它是在Excel中完成的,可以在解决方案下载中找到(在测试解决方案的 _Documents 文件夹下)。我试图涵盖所有基本类型(又名公共语言运行时类型)、它们的可为空的挂件(例如int?)及其null本身。这个矩阵是我在实际编程之前编写的单元测试的基础。如果您查看测试——不要惊慌:超过1.500个测试中的大多数都是由我编写的一个小程序构建的,而不是手动设置的。对于这种结构化测试,通过代码生成工具创建它们可能很容易。矩阵中没有提到——但很有用——因此添加到测试中——我也期待着支持Enum。除了测试之外,我还设置了由我想要的现成稳定泛型方法ConvertTo<T>组成的UniversalTypeConverter——类。这样,所有生成的测试都有效——但当然失败了。所以我已经准备好进行实际的编程,目标是得到一个又一个闪亮的绿色测试。
关于守则
以防万一您期待查看源代码,我将解释其结构,简而言之:main方法是最重载的TryConvert版本。大多数其他public方法将其工作委托给此方法。当然,这种方法也将部分工作委托给私人助手。因为我实现了 Try模式,所以这些帮助程序方法中的大多数都必须遵循这个概念并返回布尔值,同时将转换后的值作为out-parameter处理。
该代码使用代码契约和FluentAssertions进行测试。因此,您必须免费安装它们,以便编译和测试源代码。但别担心,最终的DLL可以在bin文件夹中找到——随时可用。
让我们从简单开始
在开始时会检查一些简单的方案。您将在上述TryConvert版本——方法中找到代码。如果目标类型是object——一切都是object——则无需执行,因此我们可以将其返回。如果输入类型可分配给请求的目标,无论是通过实现请求的接口还是从请求的类型派生——或者只是作为请求的类型本身,这就是解决方案。
看看代码:
if (destinationType == typeof(object)) {
result = value;
return true;
}
if (ValueRepresentsNull(value)) {
return TryConvertFromNull(destinationType, out result, options);
}
if (destinationType.IsAssignableFrom(value.GetType())) {
result = value;
return true;
}
Null
您会注意到上面的代码中的null处理。ValueRepresentsNull——方法检查值是否为null或DBNull.Value。在使用数据库操作时,在ADO.NET世界中DBNull表示正常的null。
我认为这是每个人第一次和第二次处理数据库时偶然发现的事情。所以我对待两者的null方式是一样的。
如果有的为null,我们将不得不处理它——就像在TryConvertFromNull中:
result = GetDefaultValueOfType(destinationType);
if (result == null) {
return true;
}
return (options & ConversionOptions.AllowDefaultValueIfNull) ==
ConversionOptions.AllowDefaultValueIfNull;
GetDefaultValueOfType——方法显示给定类型的默认值:
return type.IsValueType ? Activator.CreateInstance(type) : null;
这是有效的,因为每个ValueType都有一个无参数构造函数和所有其他类型的支持的null。
如果默认值是null,我们就完成了——因为null转换后一直为null。
否则——这意味着对于所有ValueType(例如,int的默认值为 0)——只有在您接受非null默认值显式时才有可能。这是可由ConversionOptionAllowDefaultValueIfNull控制的。一些public方法接受进一步的选项——AllowDefaultValueIfNull是其中之一。其他选项将在后面介绍。目前,我认为最好有一个选项来允许null转换为不可为空的类型。
类型转换器(TypeConverter)
下面是类型转换器的时间。MSDN这样描述它:“提供将值类型转换为其他类型的统一方法”。显然,我们所需要的一切!
主要思想是,你可以定义你自己的TypeConverter,并通过将TypeConverterAttribute设置为你的类来将它们分配给你自己的类型。这样,每个人都可以通过简单地调用TypeDescriptor.GetConverter来获得正确的TypeConverter。了解TypeConverter后,您可以使用方法CanConvertFrom、CanConvertTo、ConvertFrom和ConvertTo进行验证和转换。这真的是一种通用的方式!如果您请求的类型受支持,则已完成。
所以private TryConvertByDefaultTypeConverters方法的代码看起来非常简单:
TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
if (converter != null) {
if (converter.CanConvertFrom(value.GetType())) {
try {
result = converter.ConvertFrom(null, culture, value);
return true;
}
catch {
}
}
}
converter = TypeDescriptor.GetConverter(value);
if (converter != null) {
if (converter.CanConvertTo(destinationType)) {
try {
result = converter.ConvertTo(null, culture, value, destinationType);
return true;
}
catch {
}
}
}
return false;
你问自己,如果转换已经被CanConvertFrom/To调用检查了,为什么要使用try-catch?因为CanConvertFrom/To只检查转换的可能性!一般来说,可以将string转换为DateTime——但实际上它取决于string的值,例如“Hello World”并不是真正的DateTime,尝试转换它会引发异常。例如TryConvertFrom,如果TypeConverter实现了Try模式,那就太好了,但遗憾的是事实并非如此。因此,我们必须自己抓住这个可能的错误。
给定的文化用于全球化——我们稍后会谈到这一点。
目前,.NET Framework附带了许多针对不同类型的预定义TypeConverter,并且这种技术被序列化大量使用——例如,将各种类型放入aspx页的ViewState中,该页仅包含一个string。带着这个礼物,我开始了我的测试,期待看到一切都闪耀着绿色......
IConvertible
但一些测试仍然失败了。例如,int不能转换为decimal。我有点困惑,因为这些测试使用的是基本类型,我希望.NET Framework为每个基本类型提供一个合适的TypeConverter——而不仅仅是其中一些!但这只是一个期望...
所以我打开了一个反编译器(例如ILSpy),并查看了mscorlib-assembly中Convert类的不同ConvertTo方法。瞧——我遇到了 IConvertible 接口。MSDN对此接口的说明如下:“定义将实现引用或值类型的值转换为具有等效值的公共语言运行库类型的方法”。不要问我为什么——但很明显,该框架实现了多种处理转换的方法......很明显,我们也将不得不考虑它们。
因此——作为一种回退——如果没有可以完成这项工作的TypeConverter,则将值传递给TryConvertByIConvertibleImplementation方法。检查给定值的类型以实现IConvertible。此接口为每个受支持的目标类型提供显式 ToX 方法。所以只有不太优雅的方式来定义一些ifthen——块。同样,没有Try模式。所以它必须在try-catch中。
总而言之,代码如下所示:
if (value is IConvertible) {
try {
if (destinationType == typeof(Boolean)) {
result = ((IConvertible)value).ToBoolean(formatProvider);
return true;
}
if (destinationType == typeof(Byte)) {
result = ((IConvertible)value).ToByte(formatProvider);
return true;
}
if (destinationType == typeof(Char)) {
result = ((IConvertible)value).ToChar(formatProvider);
return true;
}
if (destinationType == typeof(DateTime)) {
result = ((IConvertible)value).ToDateTime(formatProvider);
return true;
}
if (destinationType == typeof(Decimal)) {
result = ((IConvertible)value).ToDecimal(formatProvider);
return true;
}
if (destinationType == typeof(Double)) {
result = ((IConvertible)value).ToDouble(formatProvider);
return true;
}
if (destinationType == typeof(Int16)) {
result = ((IConvertible)value).ToInt16(formatProvider);
return true;
}
if (destinationType == typeof(Int32)) {
result = ((IConvertible)value).ToInt32(formatProvider);
return true;
}
if (destinationType == typeof(Int64)) {
result = ((IConvertible)value).ToInt64(formatProvider);
return true;
}
if (destinationType == typeof(SByte)) {
result = ((IConvertible)value).ToSByte(formatProvider);
return true;
}
if (destinationType == typeof(Single)) {
result = ((IConvertible)value).ToSingle(formatProvider);
return true;
}
if (destinationType == typeof(UInt16)) {
result = ((IConvertible)value).ToUInt16(formatProvider);
return true;
}
if (destinationType == typeof(UInt32)) {
result = ((IConvertible)value).ToUInt32(formatProvider);
return true;
}
if (destinationType == typeof(UInt64)) {
result = ((IConvertible)value).ToUInt64(formatProvider);
return true;
}
}
catch {
return false;
}
}
return false;
不要介意formatProvider ,但——它再次用于全球化,如下所述。
所以这应该完成工作,不是吗?
没有异常就没有异常
错!即使有IConvertible,例如double也不能转换为char。也许这听起来很明显,因为你真的无法处理1.23为char。但是您也不能处理400(int)作为一个char(因为超出范围);但是,.NET Framework支持从int到char的开箱即用的转换。你知道我的意思?为什么不将1.00转换为char?因此,在TryConvertByIntermediateConversion——方法中通过中间类型处理了一些特殊情况:
if (value is char && (destinationType == typeof(double) || destinationType == typeof(float))) {
return TryConvertCore(System.Convert.ToInt16(value),
destinationType, ref result, culture, options);
}
if ((value is double || value is float) && destinationType == typeof(char)) {
return TryConvertCore(System.Convert.ToInt16(value),
destinationType, ref result, culture, options);
}
return false;
这里没有太多要讨论的——它只需要弄清楚。
隐式的——但显式的!
起初,通过中间类型转换涵盖了更多的情况。但是在发布本文的第一个版本后,leppie 向我指出了隐式和显式转换运算符——谢谢leppie!
那么什么是隐式转换和显式转换呢?
当您尝试从较小的整型转换为较大的整型或从派生类转换为基类时,将完成隐式转换——嗯,隐式转换。
这就是为什么你可以写如下的东西:
int a = 123;
float b = a;
显式转换需要您提供一些特殊的语法,以表明您知道自己在做什么。这是因为信息可能会在转换过程中丢失。例如,将一个数值类型转换为精度较低或范围较小的另一种类型。这种特殊的语法称为“强制转换”,您必须使用强制转换运算符——即将表达式放在大括号中。如果你看它,你会认出它,肯定的:
float a = 123;
int b = (int)a;
这一切都有效,因为为每个转换定义了特殊的static方法。这些方法由运算符关键字标记,因此存在隐式运算符和显式运算符。编译器会将您的强制转换(如上例所示)定向到这些方法。这就是为什么它如此顺利地集成到语法中的原因。也许这就是为什么我没有将它们视为转换方法的原因。
顺便说一下,这些operator方法还负责其他操作——例如,加法。
因此,如果我们想在UniversalTypeConverter中使用它们,我们必须自己管理这些调用。但是在哪里可以找到它们呢?我发现这些operator方法被编译为普通方法,并被添加到受影响的类型中。按照惯例,这些方法始终命名为“op_Implicit”或“op_Explicit”。你可以通过检查 mscorlib.dll 中的Decimal类型来使用反编译器查看这一点。
为了根据给定的输入和目标类型调用这些方法的正确版本,我们使用反射,因为它在TryConvertXPlicit方法中完成:
private static bool TryConvertXPlicit(object value, Type invokerType,
Type destinationType, string xPlicitMethodName, ref object result) {
var methods = invokerType.GetMethods(BindingFlags.Public | BindingFlags.Static);
foreach (MethodInfo method in methods.Where(m => m.Name == xPlicitMethodName)) {
if (destinationType.IsAssignableFrom(method.ReturnType)) {
var parameters = method.GetParameters();
if (parameters.Count() == 1 &&
parameters[0].ParameterType == value.GetType()) {
try {
result = method.Invoke
(null, new[] { value });
return true;
}
// ReSharper disable EmptyGeneralCatchClause
catch {
// ReSharper restore EmptyGeneralCatchClause
}
}
}
}
return false;
}
此方法检查具有给定operatorMethodName(“op_Explicit”或“op_Implicit”)的每个方法的所需类型,如果找到,则调用正确的方法。它被调用自TryConvertXPlicit。一次用于value的类型,一次用于destinationType。那是因为我们不知道哪种类型定义了正确的operator方法。查看代码:
private static bool TryConvertXPlicit(object value, Type destinationType,
string operatorMethodName, ref object result) {
if (TryConvertXPlicit(value, value.GetType(),
destinationType, operatorMethodName, ref result)) {
return true;
}
if (TryConvertXPlicit(value, destinationType,
destinationType, operatorMethodName, ref result)) {
return true;
}
return false;
}
因此,这通过UniversalTypeConverter为我们提供了另一种通用的转换方式。
枚举
这时,我不再相信一种一贯的皈依方式。我没有失望。
尝试使用到目前为止描述的方法转换int为Enum。它不起作用——您必须使用Enum的static方法 ToObject,因为它在TryConvertToEnum方法中完成:
try {
result = Enum.ToObject(destinationType, value);
return true;
}
catch {
return false;
}
可为空的类型
Null再一次?我知道我们已经谈过null。但是还记得像int这样的ValueType不能为null的问题吗?从.NET的2.0版开始,有一个包装器类型提供一种null对ValueType的赋值。同样,我考虑了数据库背景——数据库能够存储null在——int列中。如果必须将这些null或其他null存储在.NET端,则应仔细查看所谓的可为null的类型。达到这一点,我并不感到惊讶,到目前为止描述的任何方法都没有处理从/到这些类型的转换。
如上所述,可为空的类型是实际类型的包装器。所以我想到了在转换之前解开实际类型的包装。这样,我们可以按原样使用所有描述的方法。很高兴,返回到可空类型的方法——如果请求——由框架隐含完成——所以没有什么可做的了。
因此,您可以看到转换的核心类型在TryConvert 中定义:
Type coreDestinationType = IsGenericNullable(destinationType) ?
GetUnderlyingType(destinationType) : destinationType;
帮助程序方法检查是否使用了可为null的类型并获取基础类型(如果是):
IsGenericNullable
return type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>).GetGenericTypeDefinition();
GetUnderlyingType
return Nullable.GetUnderlyingType(type);
从这里开始,一切都像一个正常的 ValueType。
不要害怕,如果您不能像其他部分那样轻松地遵循本节。如果您以前没有听说过泛型和可为空,那就不那么容易了。如果您有兴趣,可以自己学习这些内容并再次阅读该部分。但是现在,请继续阅读本文。
完成
信不信由你,这样所有预期的转换都有效——万岁!因此,我们可以看看转换过程中需要注意的一些一般事项,以及通用类型转换器如何为您提供支持。
放眼全球
将string转换为另一种类型通常取决于给定的区域性。例如,您可以将1000 读作一千。但是在德国,它的意思是小数点后有三位数字,因为小数点在这里是一个逗号——很混乱,不是吗?因此,转换——例如转换为decimal——将返回不同的结果。因此,每个方法都有一个重载,您可以在其中指定区域性。这就是为什么我们使用文化或formatProvider如上所述的辅助方法。除非另有说明,否则基本上使用当前区域性。如果您要走向国际,您应该真正注意为数字和DateTime转换定义适当的文化——这是一个无声的错误,因为转换是无一例外地完成的,但结果是错误的!
使用代码
只需引用DLL并将其与最终的二进制文件一起分发即可。UniversalTypeConverter作为命名空间TB.ComponentModel中的static类型出现。因此,您可以在不创建实例的情况下使用它:
decimal myValue = UniversalTypeConverter.ConvertTo<decimal>(myStringValue);
如果不确定该类型是否可以转换为另一种类型,则可以使用TryConvertTo——方法:
decimal result;
bool canConvert = UniversalTypeConverter.TryConvertTo<decimal>(myStringValue, out result);
它遵循 Try模式,因此如果类型已转换,它会返回true,并且您可以从out-参数读取转换后的值。如果未转换类型,则返回false。那么out-参数是你应该忽略的。
如果您只想检查类型是否可转换——对结果不感兴趣——则可以使用CanConvertTo——方法 而不定义out-参数:
bool canConvert = UniversalTypeConverter.CanConvertTo<decimal>(myStringValue);
如果您在编译时知道类型,则所有这些泛型方法都可以正常工作。如果在运行时获取类型,则必须使用非泛型版本——将目标类型作为参数:
... = UniversalTypeConverter.Convert(myStringValue, requestedType);
扩展方法
此外,UniversalTypeConverter还附带了一组object类型的扩展方法。这样,您可以更舒适地使用它:
decimal myValue = myStringValue.ConvertTo<decimal>(CultureInfo.CurrentCulture);
选项
实践表明,如果需要,您必须自己指定一些选项。在放眼全球部分中,我已经提到文化是一种选择。其他选项由ConversionOptions enum指定。这个enum被定义为一个标志,因此您可以根据需要组合这些选项:
- AllowDefaultValueIfNull:如果给定值为null并且目标类型不支持,则返回给定目标类型的默认值null(如 Null-部分所述)。
- AllowDefaultValueIfWhitespace:如果给定值是仅包含空格但不支持从空格转换的string,则返回给定目标类型的默认值。
- EnhancedTypicalValues:允许从对我来说有意义的典型值转换。例如,将string “True”转换为开箱即用的bool。但我决定“Yes”、“No”等也是可转换的。您可以按照TryConvertSpecialValues——方法 EnhancedTypicalValues查看这些内容。
除非另有说明,否则转换将使用以下设置完成:
- CultureInfo取自CultureInfo.CurrentCulture
- ConversionOptions默认使用EnhancedTypicalValues
NuGet包
UniversalTypeConverter也可以作为 NuGet 包提供。只需在NuGet包管理器中进行搜索UniversalTypeConverter即可。
.net标准/核心
最后一个NuGet-Package支持.net Standard >= 1.3,因此可以在.net Core中使用。
感谢Stefan Ossendorf为此付出了一些努力!
IEnumerable——数组、列表、集合等
使用UniversalTypeConverter已经表明可以扩展它以简化数组或列表的工作。为了保持通用性,我们将在通用中谈论IEnumerable。因此,现在包括三个新功能:
- 将IEnumerable的所有元素转换为指定类型(例如int[]到string[])。
- 将IEnumerable的所有元素转换为字符串表示形式(例如“1,2,3”)。
- 拆分字符串(例如“1,2,3”)并将所有子字符串转换为指定类型的IEnumerable。
使用IEnumerable和IEnumerable<T>可以让您轻松地与Linq交互。所以我实现了一个很小的流畅接口来配置转换——只是为了保持与Linq的同步。
我将为这三个新功能中的每一个提供一个注释示例。大多数选项都是——嗯,可选的。我将向您展示最大配置。您可以根据需要将其缩减。
1. 从IEnumerable转换为可枚举<T>()
string[] sourceValues = new[] { "12", null, "118", "xyz" };
int[] convertedValues = sourceValues.ConvertToEnumerable<int>()
// null values in the input are ignored.
.IgnoringNullElements()
// Values which are not convertible to the destination type are ignored.
.IgnoringNonConvertibleElements()
// Specifies the culture to use - have a look above in this article.
.UsingCulture(CultureInfo.CurrentCulture)
// Specifies the options to use - have a look above in this article.
.UsingConversionOptions(ConversionOptions.AllowDefaultValueIfWhitespace)
// You can continue with basic Linq:
.ToArray();
这将导致一个包含两个元素(12和118)的int数组。
2. ConvertToStringRepresentation()
object[] input = new object[] { 2, null, true, "Hello world!", ""};
string stringRepresentation = input
.ConvertToStringRepresentation(
// Specifies the culture to use - have a look above in this article.
CultureInfo.CurrentCulture,
// Specified an IStringConcatenator which uses the semicolon
// as seperator, shows null values as "null" and ignores empty values.
new GenericStringConcatenator(";", ".null.", ConcatenationOptions.IgnoreEmpty)
);
这将返回字符串“2;.Null.;True; Hello world!”。
默认情况下,您可以忽略IStringConcatenator。存在用于简化分隔符和null值定义的重载。默认情况下,它使用分号和“.null”。但是,可能会出现您想要指定有关如何构建结果字符串的更多详细信息的情况。然后,您可以创建自己的IStringConcatenator传递到转换方法。如果需要,您可以为此研究代码。
3. ConvertToEnumerable<T>()
这是ConvertToStringRepresentation()的哥们,看起来像这样:
string input = "1;;.null.;3;xyz";
int[] result = input.ConvertToEnumerable<int>(new GenericStringSplitter(";"))
// Specifies an IStringSplitter which splits the input by semicolon.
// Trims the end of each splitted element before conversion.
.TrimmingEndOfElements()
// Trims the start of each splitted element before conversion.
.TrimmingStartOfElements()
// Values which are empty are ignored.
.IgnoringEmptyElements()
// Specifies how null values are represented within the unput string.
.WithNullBeing(".null.")
// Values which are null are ignored.
.IgnoringNullElements()
// Values which are not convertible to the destination type are ignored.
.IgnoringNonConvertibleElements()
// Specifies the options to use - have a look above in this article.
.UsingConversionOptions(ConversionOptions.AllowDefaultValueIfWhitespace)
// Specifies the culture to use - have a look above in this article.
.UsingCulture(CultureInfo.CurrentCulture)
// You can continue with basic Linq:
.ToArray();
这将生成一个包含两个元素(1 和 3)的int数组。
同样,IStringSplitter适用于更复杂的方案。您可以创建自己的类以支持所需的格式(例如拆分csv行)。
正在处理IEnumerable时,您可以调用Try()而不是ToArray()。如果转换有效并将结果作为out参数处理,这将返回true。
就是这样——感谢您继续阅读。顺便说一下——NuGet包也已更新。所以不要忘记更新!
局限性
转换只有在使用的类型提供上述接口时才有效——因此继承自TypeConverter、实现IConvertible或提供适当的operator方法。但是TypeConverter背后的概念应该适合你自己的所有类型。更糟糕的是,这些类型应该以某种方式兼容——当然,将TextBox转换为bool是行不通的。
我经常提到我实现了Try模式。如果你深入研究这一点,你会读到它应该在不使用try-catch的情况下实现。那是因为性能。但是你会看到我把转换包装在try-catch中。那是因为TypeConverter和IConvertible缺少适当的TryConvert。
结论
.NET不提供跨所有类型的转换的通用方法。甚至基本类型的处理方式也不相同。并且,并非支持跨基本类型的所有可能转换。我们研究了不同的技术,最终将UniversalTypeConverter作为通用解决方案填补了空白,并在顶部提供了一些有用的选项。也许,这是一个历史的东西,TypeConverter并没有在所有类型上使用。但是,如果您想提供自己的可转换类型,这绝对是要走的路。
总而言之,我希望这篇文章对您有所帮助。如果是这样,如果你投票支持它就好了。
如果你偶然发现UniversalTypeConverter不支持的转换,请不要犹豫,给我写信——当然,除了这样TextBox或bool类似的东西......
https://www.codeproject.com/Articles/248440/Universal-Type-Converter