创建简单高效的值类型的模式
动机:
阅读 Mark Seemann 的博客 代码气味:自动财产他在接近尾声时说道:
底线是自动属性很少适用。 事实上,它们仅适用于属性类型为 值类型和所有可以想象的值都是允许的。
他以 intTemperature
作为难闻气味的示例,并建议最好的解决方案是单位特定值类型,例如摄氏度。因此,我决定尝试编写一个自定义的摄氏度值类型,该类型封装了所有边界检查和类型转换逻辑,作为更加固体。
基本要求:
- 不可能有无效值
- 封装转换操作
- 高效应对(相当于int的替换)
- 使用起来尽可能直观(尝试int的语义)
实现:< /strong>
[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 为常量而不是只读? 查看 BCL我喜欢 int 的元数据定义清楚地说明了 MaxValue 和MinValue 作为编译时常量。我怎样才能模仿呢?我没有看到一种方法可以在不调用构造函数或公开Celsius存储int的实现细节的情况下创建Celsius对象。
- 我是否缺少任何可用性功能?
- 是否有更好的模式来创建自定义单字段值类型?
Motivation:
In reading Mark Seemann’s blog on Code Smell: Automatic Property he says near the end:
The bottom line is that automatic properties are rarely appropriate.
In fact, they are only appropriate when the type of the property is a
value type and all conceivable values are allowed.
He gives int Temperature
as an example of a bad smell and suggests the best fix is unit specific value type like Celsius. So I decided to try writing a custom Celsius value type that encapsulates all the bounds checking and type conversion logic as an exercise in being more SOLID.
Basic requirements:
- Impossible to have an invalid value
- Encapsulates conversion operations
- Effient coping (equivalent to the int its replacing)
- As intuitive to use as possible (trying for the semantics of an int)
Implementation:
[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...
}
Tests:
[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");
}
}
}
Questions:
- Is there a way to make MinValue/MaxValue const instead of readonly? Looking at the BCL I like how the meta data definition of int clearly states MaxValue and MinValue as compile time constants. How can I mimic that? I don’t see a way to create a Celsius object without either calling the constructor or exposing the implementation detail that Celsius stores an int.
- Am I missing any usability features?
- Is there a better pattern for creating a custom single field value type?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
不。但是,BCL 也不这样做。例如,DateTime.MinValue 是
静态只读。您当前的方法对于
MinValue
和MaxValue
是合适的。至于你的另外两个问题——可用性和模式本身。
就我个人而言,我会避免像这样的“温度”类型的自动转换(隐式转换运算符)。温度不是整数值(事实上,如果您要这样做,我认为它应该是浮点 - 93.2 摄氏度是完全有效的。)将温度视为整数,特别是将任何整数值隐式地视为温度似乎不合适,并且是错误的潜在原因。
我发现具有隐式转换的结构通常会导致比它们解决的问题更多的可用性问题。强制用户写:
并不比从整数隐式转换困难多少。然而,情况要清楚得多。
No. However, the BCL doesn't do this, either. For example, DateTime.MinValue is
static readonly
. Your current approach, forMinValue
andMaxValue
is appropriate.As for your other two questions - usability and the pattern itself.
Personally, I would avoid the automatic conversions (implicit conversion operators) for a "temperature" type like this. A temperature is not an integer value (in fact, if you were going to do this, I would argue that it should be floating point - 93.2 degrees C is perfectly valid.) Treating a temperature as an integer, and especially treating any integer value implicitly as a temperature seems inappropriate and a potential cause of bugs.
I find that structs with implicit conversion often cause more usability problems than they address. Forcing a user to write:
Is not really much more difficult than implicitly converting from an integer. It is far more clear, however.
我认为从可用性的角度来看,我会选择类型
Temperature
而不是Celsius
。摄氏度
只是一个测量单位,而温度
代表实际测量值。然后您的类型可以支持多种单位,例如摄氏度、华氏度和开尔文。我还会选择十进制作为后备存储。沿着这些思路:
我会避免隐式转换,因为里德指出它使事情变得不那么明显。不过,我会重载运算符(<、>、==、+、-、*、/),因为在这种情况下,执行此类操作是有意义的。谁知道呢,在 .net 的未来版本中,我们甚至可能能够指定运算符约束,并最终能够编写更多可重用的数据结构(想象一个统计类,它可以计算支持 +、-、*、 /)。
I think from a usability point of view I would opt for a type
Temperature
rather thanCelsius
.Celsius
is just a unit of measure while aTemperature
would represent an actual measurement. Then your type could support multiple units like Celsius, Fahrenheit and Kelvin. I would also opt for decimal as backing storage.Something along these lines:
I would avoid the implicit conversion as Reed states it makes things less obvious. However I would overload operators (<, >, ==, +, -, *, /) as in this case it would make sense to perform these kind of operations. And who knows, in some future version of .net we might even be able to specify operator constraints and finally be able to write more reusable data structures (imagine a statistics class which can calculate statistics for any type which supports +, -, *, /).
DebuggerDisplay
是有用的触摸。我会添加测量单位“{m_value} C”,以便您可以立即看到类型。根据目标用途,除了具体类之外,您可能还希望拥有与基本单元之间的通用转换框架。即以 SI 单位存储值,但能够根据文化显示/编辑,例如(摄氏度、公里、千克)与(华氏度、英里、磅)。
您还可以查看 F# 测量单位以获取其他想法 ( http://msdn.microsoft .com/en-us/library/dd233243.aspx ) - 请注意,它是编译时构造。
DebuggerDisplay
is useful touch. I'd add unit of measurements "{m_value} C" so you can immediately see the type.Depending on target usage you may also want to have generic conversion framework to/from base units in addtion to concrete classes. I.e. store values in SI units, but be able to display/edit based on culture like (degrees C, km, kg) vs. (degrees F, mi, lb).
You may also check out F# measurement units for additioanl ideas ( http://msdn.microsoft.com/en-us/library/dd233243.aspx ) - note that it is compile time construct.
我认为这是一个非常好的值类型实现模式。我过去也做过类似的事情,效果很好。
只是一件事,由于
Celsius
无论如何都可以隐式地转换为int
或从int
转换,因此您可以像这样定义边界:但是,实际上
static 之间没有实际区别只读
和const
。I think this is a perfectly fine implementation pattern for value types. I've done similar things in the past that have worked out well.
Just one thing, since
Celsius
is implicitly convertible to/fromint
anyway, you can define the bounds like this:However, in reality there's no practical difference between
static readonly
andconst
.