轻量级 .NET 语义类型






4.98/5 (25投票s)
一种为数据成员添加语义、强类型检查和验证的高效解决方案。
更新于 10 月 10 日:v 1.1:添加了性能基准测试和其他类型支持
引言
而不是写:
int distance = 10;
int period = 1;
double temperature = 37.1;
String email = "john.smith@company.com";
如果我们能这样写,会不会更好?
Integer<meter> distance = 10;
Integer<Milliseconds> period = 1;
Double<Celsius> temperature = 37.1;
String<Email> = "john.smith@company.com";
通过这种方式,我们可以指定含义(“语义”)和测量单位。编译器还可以阻止我们编写这样的愚蠢代码。
distance = period
这正是本文提出的解决方案,它具有:
- 易于使用
- 内存效率高(使用值类型
struct
) - 用途广泛,因为它可以像普通的
int
和double
字段一样用于 WPF 绑定和序列化 - 可扩展,因为还可以添加自定义验证
背景
今年 Code Project 上已经提出了几种语义类型的解决方案。一月份,Matt Perdeck 在优秀的《Introducing Semantic Types in .NET》一文中提出了语义类型的概念和动机。最近,Marc Clifton 也在《Strong Type-checking with Semantic Types》一文中提出了他的解决方案。在本文中,我将简要介绍一种替代解决方案,该方案利用值类型 struct
来节省内存和时间。该解决方案还支持自定义验证规则,并且重要的是,内置了值转换支持,以支持 WPF 中的直接绑定。
有关语义类型为何是个好主意的介绍,请阅读上面的引文。
Using the Code
要创建一个新的语义类型,您首先必须通过为此创建类来声明它。
class Celsius : SemanticType { }
然后,您可以像使用任何其他类型一样,在其中一个泛型语义值类型中使用它,不同之处在于您拥有内置的测量单位文档。
class Example {
public Double<Celsius> CurrentTemperature { get; set; }
public void IncreaseTemperatureBy(Double<Celsius> increment) {
CurrentTemperature += increment;
}
public Integer<Celsius>? TargetTemperature { get; set; }
}
验证
如果需要,您还可以通过验证来自定义您的语义类型。如果您想确保值在范围内,可以重写 IsValid
函数。
public class Celsius : SemanticType
{
public const double MinValue = -273.15;
public override bool IsValid(ref double doubleValue) {
return doubleValue >= MinValue;
}
public override bool IsValid(ref int value) {
return value >= MinValue;
}
public override string GetInvalidMessage(object invalidValue) {
return "Temperature in Celsius must always be higher than or equal to -273.15 degrees";
}
public static String HelpText {
get { return "Temperature in degress Kelvin."; }
}
}
如果您想对不同基类型(例如,double 和 integer)使用相同的语义类型,您必须重写相应的 IsValid
函数。值通过 ref
参数发送到 IsValid
函数,以允许该函数调整值。
如您所见,您还可以提供自定义错误消息。这用作异常中返回的消息,并且可以在代码中访问。可以添加其他成员,以便从 Double<T>
或 Integer<T>
的 Semantic
属性访问,该属性返回 SemanticType
的实例。
WPF 中的数据绑定
为了支持在 WPF 绑定源中直接用作绑定源,语义类型具有自定义类型转换器。这意味着您可以在绑定中使用 Double<TSemantic>
的属性,就像使用 double
一样。此外,您可以通过 Semantic
属性绑定到 SemanticType
的其他属性。也许您想为滑块控件提供 Maximum
和 Minimum
值?或者默认值?
性能
显然,语义类型不可能像普通的内置基本类型那样高效,但请记住,验证支持已包含在内,并且您可以使代码免受错误。我包含了一个简单的基准测试,该测试创建并操作数组项。在我的计算机上,在 Release 模式下进行调试时,典型结果如下:
Result for 1000000 iterations:
Double: 4 ms
Semantic: 15 ms
ByRef: 166 ms
如您所见,在这个您只做了少量其他操作的特定案例中,原始操作语义类型(Double<T>
)花费的时间大约是普通 double
的 4-5 倍。当然,在有大量数字计算的应用中必须考虑这一点。然而,在您做了许多其他事情的普通应用中,这应该是微不足道的。同时请记住,通过使用语义替代方案,数据的内存消耗不会增加任何一点。
作为比较,我还包括了一个简单的“语义” ByRef
类型测试,即包含一个值的类。如您所见,以这种方式初始化和操作数据要慢得多,并且当然会消耗更多的内存空间。
支持其他数据类型
我在这里提供了对 Integer
、Double
和 String
的支持,这对于许多普通应用程序来说应该足够了。为了支持其他数据类型,如 long
,还包含了一个泛型 Data<TData,TSemantic>
类型。要使用它,您只需实现一个实现 ISemanticType<TData>
接口的 TSemantic
类。请参阅提供的代码中的示例。
关注点
使用值类型结构体的低内存消耗
所有语义类型都是值类型 struct
,它们有一个实例变量来存储值。这意味着它们在内存中消耗的字节数不比封装的基础类型多。与使用类类型的解决方案相比,这减少了内存消耗和垃圾回收器的压力。语义值类型与普通基本类型(按值传递)一样是不可变的。
泛型类型的自定义类型转换器
为了支持直接数据绑定,我必须为语义类型提供自定义 TypeConverter
。对于泛型类型,这有点挑战,但在阅读了一篇 有价值的 StackOverflow 帖子回答 后,很容易创建了一个泛型解决方案,该解决方案可以重新用于任何需要具有相应泛型类型转换器(具有相同的类型参数)的泛型类型。要使用它,您只需用属性装饰该类型,首先使用普通的 TypeConverter
属性指定 GenericTypeConverter
类型作为转换器类型,然后使用附加属性指定要使用的实际泛型类型转换器类型。当 WPF 需要类型的类型转换器时,将创建一个 GenericTypeConverter
的新实例。在其构造函数中,将创建一个泛型类型转换器的实例。然后将所有请求转发给泛型类型转换器。
结论
现在我们有了另一个用于 .NET 语义类型的微型解决方案。您怎么看?它有用吗?也许 .NET 平台可以包含对所有基本类型的泛型版本类似的支持?这可以通过在即时编译期间丢弃语义检查来实现,从而可能更高效。
历史
- 2015 年 10 月 8 日 - 发布第一个版本
- 2015 年 10 月 10 日 - 添加了性能基准测试和其他类型支持。(v.1.1)
- 2015 年 10 月 13 日 - 对文章文本进行了少量更正(代码无更改)