创建简单高效值类型的模式

Posted

技术标签:

【中文标题】创建简单高效值类型的模式【英文标题】:Pattern for Creating a Simple and Efficient Value type 【发布时间】:2011-12-23 20:51:43 【问题描述】:

动机:

在阅读 Mark Seemann 在Code Smell: Automatic Property 上的博客时,他在接近尾声时说:

底线是自动属性很少适用。 实际上,它们仅适用于属性类型为 值类型和所有可能的值都是允许的。

他以int Temperature 为例说明难闻的气味,并建议最好的解决方法是特定于单位的值类型,例如摄氏度。所以我决定尝试编写一个自定义的摄氏值类型,它封装了所有的边界检查和类型转换逻辑,作为练习更多SOLID。

基本要求:

    不可能有无效值 封装转换操作 高效应对(相当于 int 替换它) 使用尽可能直观(尝试使用 int 的语义)

实施:

[System.Diagnostics.DebuggerDisplay("m_value")]
public struct Celsius // : IComparable, IFormattable, etc...

    private int m_value;

    public static readonly Celsius MinValue = new Celsius()  m_value = -273 ;           // absolute zero
    public static readonly Celsius MaxValue = new Celsius()  m_value = int.MaxValue ;

    private Celsius(int temp)
    
        if (temp < Celsius.MinValue)
            throw new ArgumentOutOfRangeException("temp", "Value cannot be less then Celsius.MinValue (absolute zero)");
        if (temp > Celsius.MaxValue)
            throw new ArgumentOutOfRangeException("temp", "Value cannot be more then Celsius.MaxValue");

        m_value = temp;
    

    public static implicit operator Celsius(int temp)
    
        return new Celsius(temp);
    

    public static implicit operator int(Celsius c)
    
        return c.m_value;
    

    // operators for other numeric types...

    public override string ToString()
    
        return m_value.ToString();
    

    // override Equals, HashCode, etc...

测试:

[TestClass]
public class TestCelsius

    [TestMethod]
    public void QuickTest()
    
        Celsius c = 41;             
        Celsius c2 = c;
        int temp = c2;              
        Assert.AreEqual(41, temp);
        Assert.AreEqual("41", c.ToString());
    

    [TestMethod]
    public void OutOfRangeTest()
    
        try
        
            Celsius c = -300;
            Assert.Fail("Should not be able to assign -300");
        
        catch (ArgumentOutOfRangeException)
        
            // pass
        
        catch (Exception)
        
            Assert.Fail("Threw wrong exception");
        
    

问题:

有没有办法让 MinValue/MaxValue 变为 const 而不是只读? 看看 BCL,我喜欢 int 的元数据定义清楚地将 MaxValue 和 MinValue 声明为编译时常量。我怎么能模仿呢?如果不调用构造函数或公开Celsius 存储int 的实现细节,我看不到创建Celsius 对象的方法。 我是否缺少任何可用性功能? 是否有更好的模式来创建自定义单字段值类型?

【问题讨论】:

查看这个问题(有人回答你“缺少可用性功能”部分)-***.com/questions/441309/why-are-mutable-structs-evil 并从中链接。适用于所有值类型。 +1 回答关于变得更加 SOLID 的问题。 @Alexei - 我以前读过所有“可变结构是邪恶的”的帖子。我同意。问题是,如果我将私有字段设为只读,则 Celcius.MaxValue 会调用需要Celsius.MaxValue 已定义的构造函数。这是循环的,会导致运行时异常。这就是我在 MaxValue 定义中使用默认构造函数的原因。你知道解决这个问题的方法吗?一个特殊用途的“不检查边界”的私有构造函数感觉不对。 我没有意识到这一点。我认为拥有为给定类型创建常量的特殊方法(私有 CreateConstantValue()?)对于自我记录代码很有用 - 现在查看代码无法知道为什么必须调用默认构造函数。 【参考方案1】:

我认为从可用性的角度来看,我会选择Temperature 类型而不是CelsiusCelsius 只是一个度量单位,而Temperature 将代表一个实际度量。然后你的类型可以支持多个单位,如摄氏度、华氏度和开尔文。我也会选择十进制作为后备存储。

类似的东西:

public struct Temperature

    private decimal m_value;

    private const decimal CelsiusToKelvinOffset = 273.15m;

    public static readonly Temperature MinValue = Temperature.FromKelvin(0);
    public static readonly Temperature MaxValue = Temperature.FromKelvin(Decimal.MaxValue);

    public decimal Celsius
    
        get  return m_value - CelsiusToKelvinOffset; 
    

    public decimal Kelvin 
    
        get  return m_value; 
    

    private Temperature(decimal temp)
    
        if (temp < Temperature.MinValue.Kelvin)
               throw new ArgumentOutOfRangeException("temp", "Value 0 is less than Temperature.MinValue (1)", temp, Temperature.MinValue);
        if (temp > Temperature.MaxValue.Kelvin)
               throw new ArgumentOutOfRangeException("temp", "Value 0 is greater than Temperature.MaxValue (1)", temp, Temperature.MaxValue);
         m_value = temp;
    

    public static Temperature FromKelvin(decimal temp)
         
           return new Temperature(temp);
    

    public static Temperature FromCelsius(decimal temp)
    
        return new Temperature(temp + CelsiusToKelvinOffset);
    

    ....

我会避免隐式转换,因为 Reed 表示它会使事情变得不那么明显。但是我会重载运算符(、==、+、-、*、/),因为在这种情况下执行这些操作是有意义的。谁知道呢,在未来的 .net 版本中,我们甚至可以指定操作符约束,最终能够编写更多可重用的数据结构(想象一个统计类,它可以计算任何支持 +、-、*、 /)。

【讨论】:

+1 用于比较运算符和基本温度等级...不太确定转换属性 - 可能适用于某些情况。我会考虑使用传递自定义“测量单位”字符串的 ToString/Parse 模板 (Temperature.ToString("C"))。 @Alexei:是的,基于单位的自定义字符串解析效果很好,btdt。 我喜欢 FromKelvin() 和 FromCelsius()。我认为你和@Reed Cospsey 都正确地指出 DateTime 是一个比 int 更好的 BCL 模型来模拟存储的内容。 +1:这实际上与我当前的一些类型(包括温度类型)非常非常相似。不过,我不一定同意这里选择小数点 - 对于大多数温度的使用,double 很可能具有足够的范围和精度,这就是我使用它的原因。此外 - 附带说明,构造函数,如所写,不会编译 - 它试图将小数与温度进行比较,这需要隐式转换。我通过对 Min/Max 值使用私有 const double 来解决这个问题,并将其用于比较和构建只读结构。 @Reed:你可以重载 代替。我在代码中修复了它。【参考方案2】:

DebuggerDisplay 是有用的触摸。我会添加测量单位“m_value C”,这样您就可以立即看到类型。

根据目标使用情况,除了具体的类之外,您可能还希望拥有与基本单位之间的通用转换框架。 IE。以 SI 单位存储值,但能够根据(摄氏度、公里、公斤)与(华氏度、英里、磅)等文化进行显示/编辑。

您还可以查看 F# 测量单位以了解其他想法 (http://msdn.microsoft.com/en-us/library/dd233243.aspx) - 请注意,它是编译时构造。

【讨论】:

【参考方案3】:

我认为这是值类型的完美实现模式。我过去做过类似的事情,效果很好。

只有一件事,因为Celsius 无论如何都可以隐式转换为int 或从int 转换,您可以像这样定义边界:

public const int MinValue = -273;
public const int MaxValue = int.MaxValue;

但是,实际上static readonlyconst 之间没有实际区别。

【讨论】:

我不会将 Min/MaxValue 指定为整数。它们应该是它们所包含的相同类型。想象一下 DateTime.Min/MaxValue 将是 int 类型 - 这看起来很奇怪。此外,它会泄漏内部结构(支持字段的类型),如果您想要/需要更改您的实现,这不是一件好事。 Const 是编译时间。只读是运行时。在这种情况下差异很大,因为创建只读 MaxValue 需要构造函数调用,并且构造函数依赖于已定义的 MaxValue。找到摆脱这个圈子的好方法是我发布这个问题的原因之一。【参考方案4】:

有没有办法让 MinValue/MaxValue 变为 const 而不是只读?

没有。但是,BCL 也不这样做。例如,DateTime.MinValue 是 static readonly。您当前对MinValueMaxValue 的方法是合适的。

至于您的其他两个问题 - 可用性和模式本身。

就个人而言,我会避免像这样的“温度”类型的自动转换(隐式转换运算符)。温度不是整数值(事实上,如果您要这样做,我认为它应该是浮点数 - 93.2 摄氏度是完全有效的。)将温度视为整数,尤其是隐含地将任何整数值视为温度似乎是不合适的,并且可能是导致错误的原因。

我发现带有隐式转换的结构通常会导致比它们解决的更多的可用性问题。强制用户写:

 Celsius c = new Celcius(41);

实际上并不比从整数隐式转换困难得多。然而,这要清楚得多。

【讨论】:

+1 表示写得很好,并警告在这种情况下使用int 谢谢。我明白你关于构造的隐式运算符的观点。您是否发现反向运算符隐式运算符 int(Celsius c) 存在许多问题?从清晰的角度来看,这似乎有点过头了: int i = (int)c; @ErnieL 我实际上不喜欢它 - 温度不是任意数字 - 我看不出允许隐式转换的意义,因为它往往会导致其他问题。我发现从长远来看,强制显式转换通常更安全......

以上是关于创建简单高效值类型的模式的主要内容,如果未能解决你的问题,请参考以下文章

原型模式(Prototype Pattern)

java基础之基本类型

核心数据——高效地查找或创建

JS简单数据类型和复杂数据类型

包装器类型

Zabbix Dependent items 从属依赖监控项监控类型