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

动机:

在阅读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.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对象的方法,无需调用构造函数或公开Celsius存储int的实现细节。
  • 我错过了任何可用性function吗?
  • 是否有更好的模式来创建自定义单字段值类型?

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

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

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

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

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

  Celsius c = new Celcius(41); 

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

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

DebuggerDisplay很有用。 我将添加测量单位“{m_value} C”,以便您可以立即看到类型。

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

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

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

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

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

然而,实际上static readonlyconst之间没有实际区别。