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

时间:2011-11-07 18:41:27

标签: c# design-patterns encapsulation value-type solid-principles

动机:

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

  

底线是自动属性很少适用。   实际上,只有当属性类型为a时,它们才适用   值类型和所有可以想到的值都是允许的。

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

基本要求:

  1. 无法获得无效值
  2. 封装转化操作
  3. Effient coping(相当于替换它的内容)
  4. 尽可能直观地使用(尝试int的语义)
  5. 实施

    [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而不是readonly?查看BCL我喜欢int的元数据定义如何清楚地将MaxValue和MinValue表示为编译时常量。我怎么能模仿那个?在没有调用构造函数或暴露Celsius存储int的实现细节的情况下,我没有看到创建Celsius对象的方法。
    • 我是否遗漏了任何可用性功能?
    • 是否有更好的模式来创建自定义单字段值类型?

4 个答案:

答案 0 :(得分:20)

  

有没有办法让MinValue / MaxValue const而不是readonly?

没有。但是,BCL也没有这样做。例如,DateTime.MinValuestatic readonly。您当前的方法适用于MinValueMaxValue

至于你的另外两个问题 - 可用性和模式本身。

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

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

 Celsius c = new Celcius(41);

并不比从整数隐式转换要困难得多。然而,更清楚的是。

答案 1 :(得分:9)

我认为从可用性的角度来看,我会选择Temperature类型而不是CelsiusCelsius只是一种度量单位,而Temperature则代表实际衡量标准。然后你的类型可以支持Celsius,Fahrenheit和Kelvin等多个单位。我也会选择十进制作为后备存储。

这些方面的东西:

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声称它使事情变得不那么明显。但是我会重载运算符(&lt;,&gt;,==,+, - ,*,/),因为在这种情况下执行这些操作是有意义的。谁知道,在.net的某个未来版本中,我们甚至可以指定运算符约束,最终能够编写更多可重用的数据结构(想象一个统计类,它可以计算支持+, - ,*的任何类型的统计数据, /).

答案 2 :(得分:2)

DebuggerDisplay非常有用。我会添加测量单位“{m_value} C”,这样您就可以立即看到类型。

根据目标使用情况,您可能还希望将基本单元的通用转换框架添加到具体类中。即以SI单位存储值,但能够根据(C,km,kg)与(F,mi,lb)等文化进行显示/编辑。

您也可以查看F#度量单位以获取附加想法(http://msdn.microsoft.com/en-us/library/dd233243.aspx) - 请注意它是编译时构造。

答案 3 :(得分:0)

我认为这是一种非常好的价值类型实现模式。我过去做过类似的事情已经做得很好。

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

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

然而,实际上static readonlyconst之间没有实际差异。