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

Ern*_*ieL 24 c# encapsulation design-patterns value-type solid-principles

动机:

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

底线是自动属性很少适用.实际上,只有当属性的类型是值类型并且允许所有可想到的值时,它们才适用.

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

基本要求:

  1. 不可能有无效的价值
  2. 封装转换操作
  3. 有效的应对(相当于替换它的int)
  4. 尽可能直观地使用(尝试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...
}
Run Code Online (Sandbox Code Playgroud)

测试:

[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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

问题:

  • 有没有办法使MinValue/MaxValue const而不是readonly?看看BCL我喜欢int的元数据定义如何清楚地表明MaxValue和MinValue作为编译时常量.我怎么能模仿那个?我没有看到创建Celsius对象的方法,无需调用构造函数或公开Celsius存储int的实现细节.
  • 我错过了任何可用性功能吗?
  • 是否有更好的模式来创建自定义单字段值类型?

Ree*_*sey 21

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

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

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

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

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

 Celsius c = new Celcius(41);
Run Code Online (Sandbox Code Playgroud)

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

  • @ErnieL我实际上不喜欢它 - 温度不是任意数字 - 我没有看到允许隐式转换的重点,因为它往往会导致其他问题.我发现从长远来看,强制显式转换通常更安全... (2认同)

Chr*_*Wue 9

我认为从可用性的角度来看,我会选择一种类型Temperature而不是Celsius.Celsius只是一个度量单位,而a 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);
    }

    ....
}
Run Code Online (Sandbox Code Playgroud)

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