65.9K
CodeProject 正在变化。 阅读更多。
Home

轻量级 .NET 语义类型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (25投票s)

2015 年 10 月 8 日

CPOL

5分钟阅读

viewsIcon

28854

downloadIcon

343

一种为数据成员添加语义、强类型检查和验证的高效解决方案。

更新于 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
  • 用途广泛,因为它可以像普通的 intdouble 字段一样用于 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 的其他属性。也许您想为滑块控件提供 MaximumMinimum 值?或者默认值?

性能

显然,语义类型不可能像普通的内置基本类型那样高效,但请记住,验证支持已包含在内,并且您可以使代码免受错误。我包含了一个简单的基准测试,该测试创建并操作数组项。在我的计算机上,在 Release 模式下进行调试时,典型结果如下:

Result for 1000000 iterations:
Double:   4 ms
Semantic: 15 ms
ByRef:    166 ms

如您所见,在这个您只做了少量其他操作的特定案例中,原始操作语义类型(Double<T>)花费的时间大约是普通 double 的 4-5 倍。当然,在有大量数字计算的应用中必须考虑这一点。然而,在您做了许多其他事情的普通应用中,这应该是微不足道的。同时请记住,通过使用语义替代方案,数据的内存消耗不会增加任何一点。

作为比较,我还包括了一个简单的“语义” ByRef 类型测试,即包含一个值的类。如您所见,以这种方式初始化和操作数据要慢得多,并且当然会消耗更多的内存空间。

支持其他数据类型

我在这里提供了对 IntegerDoubleString 的支持,这对于许多普通应用程序来说应该足够了。为了支持其他数据类型,如 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 日 - 对文章文本进行了少量更正(代码无更改)
© . All rights reserved.