26

动机:

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

底线是自动属性很少适用。事实上,只有当属性的类型是值类型并且所有可能的值都被允许时,它们才合适。

int Temperature举了一个难闻的气味的例子,并建议最好的解决办法是像摄氏度这样的单位特定值类型。所以我决定尝试编写一个自定义的摄氏值类型,它封装了所有的边界检查和类型转换逻辑,作为更加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...
}

测试:

[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 对象的方法。
  • 我是否缺少任何可用性功能?
  • 是否有更好的模式来创建自定义单字段值类型?
4

4 回答 4

22

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

不。但是,BCL 也不这样做。例如,DateTime.MinValuestatic readonly. 你目前的方法,适合MinValue并且MaxValue是合适的。

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

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

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

 Celsius c = new Celcius(41);

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

于 2011-11-07T18:53:45.113 回答
9

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

这些方面的东西:

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 版本中,我们甚至可以指定操作符约束,最终能够编写更多可重用的数据结构(想象一个统计类,它可以计算任何支持 +、-、*、 /)。

于 2011-11-07T20:33:38.687 回答
2

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

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

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

于 2011-11-08T02:56:16.097 回答
0

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

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

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

static readonly但是,实际上和之间没有实际区别const

于 2011-11-07T18:58:46.680 回答