使用真实世界单位而非类型

10

我的项目涉及许多包含很多现实世界单位的计算:

  • 距离;
  • 温度;
  • 流量;
  • ...

这个项目涉及复杂的和众多的计算公式。

因此,我认为使用自定义类型,如TemperatureDistance等,对于代码的可读性可能是有好处的。例如:

Temperature x = -55.3;
Meter y = 3;
或者
var x = new Temperature(-55.3);

我尝试创建一个使用双精度内部值的温度类。

public class Temperature
{
    double _Value = double.NaN;

    public Temperature() { }

    public Temperature(double v) {
        _Value = v;
    }

    public static implicit operator Temperature(double v) {
        return new Temperature(v);
    }
}

但是类是可空的。这意味着像下面这样的东西:

Temperature myTemp;

"correct"会是null,但我不想要这个。我不想使用结构体因为它们太受限制:

  • 它们不能使用无参构造函数或实例字段初始化器(例如double _Value = double.Nan;)来定义默认值(我希望默认底层double值为NaN)
  • 它们不能继承类,只能实现接口

因此,我想知道是否有一种方法可以告诉C#:

Temperature myTemp = 23K; // C# does not implement anything to make K unit...

但我知道C#无法处理没有自定义单位的情况。

Temperature myTemp = new Kelvin(23); // This might work

我想我可以创建两个继承自Temperature的Celsius和Kelvin类,但我开始思考这个想法是否真的值得,因为这需要大量的编码和测试。

这就是我想要开始讨论的问题:

在我的代码中使用真实世界的单位而不是.NET类型是否是一件好事?是否有人已经这样做过?有哪些陷阱和最佳实践?还是我最好远离这个并使用标准的.NET类型?


2
附注:F#支持本地单位 - CodesInChaos
7
你认为结构体有什么限制吗?我认为在这里使用结构体恰当无误。你的类只需要携带一个不可变的数字值 - 这正是结构体的理想用例。 - NOtherDev
1
“structs force me to override each operator”这句话是什么意思?具体有什么限制?我在参考资料中没有看到相关内容。 为什么你的Temperature可能需要从任何东西派生出来? - NOtherDev
3
对于你所需的用途,结构体是个不错的选择。你正在创建基本的数据类型。请注意,例如TimeSpan和DateTime也是结构体!此外,我可能不会创建像开尔文和华氏度这样的类型,而是只创建一个温度类型,例如开尔文,并进行转换(通过GetCelcius),就像DateTime一样。 - Pieter van Ginkel
我最近还添加了 http://github.com/InitialForce/UnitsNet。 - angularsen
显示剩余4条评论
6个回答

10

为什么不尝试一个看起来像这样的结构体:

/// <summary>
/// Temperature class that uses a base unit of Celsius
/// </summary>
public struct Temp
{
    public static Temp FromCelsius(double value)
    {
        return new Temp(value);
    }

    public static Temp FromFahrenheit(double value)
    {
        return new Temp((value - 32) * 5 / 9);
    }

    public static Temp FromKelvin(double value)
    {
        return new Temp(value - 273.15);
    }

    public static Temp operator +(Temp left, Temp right)
    {
        return Temp.FromCelsius(left.Celsius + right.Celsius);
    }

    private double _value;

    private Temp(double value)
    {
        _value = value;
    }

    public double Kelvin
    {
        get { return _value + 273.15; }
    }

    public double Celsius
    {
        get { return _value; }
    }

    public double Fahrenheit
    {
        get { return _value / 5 * 9 + 32; }
    }
}

然后像这样使用它:

    static void Main(string[] args)
    {
        var c = Temp.FromCelsius(30);
        var f = Temp.FromFahrenheit(20);
        var k = Temp.FromKelvin(20);

        var total = c + f + k;
        Console.WriteLine("Total temp is {0}F", total.Fahrenheit);
    }

这个问题在于你需要大量的类和更多的重载运算符。因为在物理学中,有很多组合单位是有用的。 - CodesInChaos
但这只是温度概念的一个类。考虑到语言的限制,你如何用更少的代码解决它?我认为这不是问题。而且,每个单位都必须进行重载。与问题中建议的使用更多类相比,这种方法可以将其最小化。 - OJ.
美丽,考虑到我们的语言限制。为了好玩,你甚至可以使_value只读。 - Joshua
很棒的解决方案。我想修改一件事,即避免重复——将常量移到常量中而不是作为类型化数字存在。 - JohnLBevan
我在Units.NET (http://github.com/InitialForce/UnitsNet) 中已经针对长度、面积、体积、质量、力、重量、压力和扭矩等方面进行了非常类似的处理。它已经过单元测试,同时支持PCL。欢迎您为温度和其他尚未添加的计量单位贡献类。 - angularsen

1
一种实现此目标的方法是使用基本对象(在您的情况下是Temperature)的组合和专门化基本对象的TemperatureTraits类。类似于C++,String等效类basic_string实际上是一个类模板(在C#术语中是通用的),它具有模板参数,不仅适用于字符串元素(char、宽字符等),还有一个特性类,详细说明了为给定类型的字符串元素(如char_traits)而设计的类的行为。
在您的情况下,您可以定义一个泛型,例如: public class MeasurableWithUnits<class M MEASURABLE, class U UNITS> 然后,实现将不仅依赖可测量类,而且还依赖单位类。这在实践中有多大用处,取决于能够使这样的对象真正通用的操作有多少-在哪些组合中MeasurableUnits是常见的?

如果这种方法看起来有趣,可以在这里找到一篇关于C# traits的研究论文。


这是一篇非常有趣的论文。不幸的是,由于时间限制和我需要专注的计算复杂性,我不认为我能将其应用于此项目。无论如何,我会点赞并收藏链接。谢谢! - Larry
1
@controlbreak - 你看过这篇相关的帖子吗?https://dev59.com/VnRC5IYBdhLWcg3wSu97 - Steve Townsend
我喜欢那个最后的链接。已接受! - Larry

0
如果您使用结构体,则无法为其分配null值。
struct Temperature 
{ 
    double _Value; 
} 

0

我认为在C#中添加静态类型来表示单位并不值得。您需要为所有的组合(不仅是所有单位)重载很多运算符。而且内建函数如Math.Sqrt也是对于普通double类型工作的,...

你可以尝试使用动态类型:

class PhysicalUnit
{
}

struct PhysicalValue
{
    readonly Value;
    readonly PhysicalUnit;
}

然后,在调试模式下编译时,添加检查以确保单位匹配。在发布中,只需删除PhysicalUnit字段和所有检查,您就可以(几乎)像使用普通double的代码一样快。


0

我认为当你想要为温度添加更具体的功能时(例如:IsFreezing()),它可能会很有用。

为了解决开尔文和摄氏度的问题:创建一个接口ITemperature和一个基类。在基类中,您可以实现接口并填写所有类相同的细节。


0

我会将Temperature作为一个抽象类,其中包含一个名为InternalTemperature的属性来存储温度(以开尔文为单位)。

派生类Celcius会将输入值内部转换为开尔文。它将具有一个(只读的)Value属性,用于将内部值转换回来。

然后比较它们(哪个更暖和)就很容易了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接