C#中的度量单位 - 差不多

时间:2008-12-08 07:32:54

标签: c# f# units-of-measurement

受到Units of Measure in F#的启发,尽管断言(here)你无法在C#中做到这一点,但我有一天知道我一直在玩的。

namespace UnitsOfMeasure
{
    public interface IUnit { }
    public static class Length
    {
        public interface ILength : IUnit { }
        public class m : ILength { }
        public class mm : ILength { }
        public class ft : ILength { }
    }
    public class Mass
    {
        public interface IMass : IUnit { }
        public class kg : IMass { }
        public class g : IMass { }
        public class lb : IMass { }
    }

    public class UnitDouble<T> where T : IUnit
    {
        public readonly double Value;
        public UnitDouble(double value)
        {
            Value = value;
        }
        public static UnitDouble<T> operator +(UnitDouble<T> first, UnitDouble<T> second)
        {
            return new UnitDouble<T>(first.Value + second.Value);
        }
        //TODO: minus operator/equality
    }
}

使用示例:

var a = new UnitDouble<Length.m>(3.1);
var b = new UnitDouble<Length.m>(4.9);
var d = new UnitDouble<Mass.kg>(3.4);
Console.WriteLine((a + b).Value);
//Console.WriteLine((a + c).Value); <-- Compiler says no

下一步是尝试实施转化(代码段):

public interface IUnit { double toBase { get; } }
public static class Length
{
    public interface ILength : IUnit { }
    public class m : ILength { public double toBase { get { return 1.0;} } }
    public class mm : ILength { public double toBase { get { return 1000.0; } } }
    public class ft : ILength { public double toBase { get { return 0.3048; } } }
    public static UnitDouble<R> Convert<T, R>(UnitDouble<T> input) where T : ILength, new() where R : ILength, new()
    {
        double mult = (new T() as IUnit).toBase;
        double div = (new R() as IUnit).toBase;
        return new UnitDouble<R>(input.Value * mult / div);
    }
}

(我本来希望避免使用静态来实例化对象,但我们都知道你can't declare a static method in an interface) 然后你可以这样做:

var e = Length.Convert<Length.mm, Length.m>(c);
var f = Length.Convert<Length.mm, Mass.kg>(d); <-- but not this

显然,与F#计量单位相比,这有一个巨大的漏洞(我会让你解决)。

哦,问题是:你怎么看待这个?值得使用吗?其他人已经做得更好吗?

更新对于对此主题领域感兴趣的人,here是1997年论文的链接,讨论了一种不同类型的解决方案(不是专门针对C#)

14 个答案:

答案 0 :(得分:41)

您缺少尺寸分析。例如(从你链接到的答案),在F#中你可以这样做:

let g = 9.8<m/s^2>

它将生成一个新的加速度单位,从米和秒中得出(你可以使用模板在C ++中实际做同样的事情)。

在C#中,可以在运行时进行维度分析,但这会增加开销,并且不会为您提供编译时检查的好处。据我所知,没有办法在C#中完成完整的编译时单元。

它是否值得做,取决于课程的应用,但对于许多科学应用来说,这绝对是个好主意。我不知道.NET的任何现有库,但它们可能存在。

如果您对如何在运行时执行此操作感兴趣,我们的想法是每个值都有一个标量值和表示每个基本单元功能的整数。

class Unit
{
    double scalar;
    int kg;
    int m;
    int s;
    // ... for each basic unit

    public Unit(double scalar, int kg, int m, int s)
    {
       this.scalar = scalar;
       this.kg = kg;
       this.m = m;
       this.s = s;
       ...
    }

    // For addition/subtraction, exponents must match
    public static Unit operator +(Unit first, Unit second)
    {
        if (UnitsAreCompatible(first, second))
        {
            return new Unit(
                first.scalar + second.scalar,
                first.kg,
                first.m,
                first.s,
                ...
            );
        }
        else
        {
            throw new Exception("Units must match for addition");
        }
    }

    // For multiplication/division, add/subtract the exponents
    public static Unit operator *(Unit first, Unit second)
    {
        return new Unit(
            first.scalar * second.scalar,
            first.kg + second.kg,
            first.m + second.m,
            first.s + second.s,
            ...
        );
    }

    public static bool UnitsAreCompatible(Unit first, Unit second)
    {
        return
            first.kg == second.kg &&
            first.m == second.m &&
            first.s == second.s
            ...;
    }
}

如果您不允许用户更改单位的值(无论如何都是个好主意),您可以为常用单位添加子类:

class Speed : Unit
{
    public Speed(double x) : base(x, 0, 1, -1, ...); // m/s => m^1 * s^-1
    {
    }
}

class Acceleration : Unit
{
    public Acceleration(double x) : base(x, 0, 1, -2, ...); // m/s^2 => m^1 * s^-2
    {
    }
}

您还可以在派生类型上定义更多特定运算符,以避免检查常见类型上的兼容单元。

答案 1 :(得分:18)

您可以在数字类型上添加扩展方法以生成度量。它感觉有点像DSL:

var mass = 1.Kilogram();
var length = (1.2).Kilometres();

这不是真正的.NET惯例,可能不是最容易发现的功能,所以也许你可以将它们添加到一个专门的命名空间中,供喜欢它们的人使用,以及提供更多的传统构造方法。

答案 2 :(得分:17)

对于相同度量的不同单位(例如,长度为cm,mm和ft)使用单独的类似乎有点奇怪。基于.NET Framework的DateTime和TimeSpan类,我希望这样:

Length  length       = Length.FromMillimeters(n1);
decimal lengthInFeet = length.Feet;
Length  length2      = length.AddFeet(n2);
Length  length3      = length + Length.FromMeters(n3);

答案 3 :(得分:10)

现在存在这样一个C#库: http://www.codeproject.com/Articles/413750/Units-of-Measure-Validator-for-Csharp

它具有与F#的单元编译时验证几乎相同的功能,但对于C#。 核心是一个MSBuild任务,它解析代码并寻找验证。

单位信息存储在注释和属性中。

答案 4 :(得分:10)

我最近在GitHubNuGet上发布了Units.NET。

它为您提供所有常用单位和转化。它重量轻,经过单元测试并支持PCL。

转换示例:

Length meter = Length.FromMeters(1);
double cm = meter.Centimeters; // 100
double yards = meter.Yards; // 1.09361
double feet = meter.Feet; // 3.28084
double inches = meter.Inches; // 39.3701

答案 5 :(得分:4)

这是我在C#/ VB中创建单元的问题。如果你认为我错了,请纠正我。我读过的大多数实现似乎都涉及创建一个将值(int或double)与单元组合在一起的结构。然后,您尝试为这些结构定义基本函数(+ - * /等),并考虑单位转换和一致性。

我发现这个想法很有吸引力,但每次我都不愿意看到这个项目的巨大一步。它看起来像一个全有或全无的交易。你可能不会只是将一些数字改为单位;重点是项目中的所有数据都用一个单元进行了适当的标记,以避免任何歧义。这意味着告别使用普通的双打和整数,现在每个变量都被定义为“单位”或“长度”或“米”等。人们真的大规模地这样做吗?因此,即使您有一个大型数组,每个元素也应该用一个单元标记。这显然会产生尺寸和性能分支。

尽管尝试将单元逻辑推入后台时非常聪明,但使用C#时,一些繁琐的符号似乎是不可避免的。 F#做了一些幕后魔术,更好地降低了单位逻辑的烦恼因素。

此外,我们如何成功地使编译器在我们需要时将单元视为普通双精度,不使用CType或“.Value”或任何其他表示法?如用nullables,代码知道处理双?就像一个double(当然,如果你的double?为null,那么你会得到一个错误)。

答案 6 :(得分:3)

感谢您的想法。我用C#实现了很多不同的方式,似乎总是有一个问题。现在我可以再次尝试使用上面讨论的想法。我的目标是能够根据现有的单位来定义新的单位,如

Unit lbf = 4.44822162*N;
Unit fps = feet/sec;
Unit hp = 550*lbf*fps

并让程序找出合适的尺寸,缩放和符号。最后,我需要构建一个基本的代数系统,它可以转换(m/s)*(m*s)=m^2之类的东西,并尝试根据定义的现有单位表达结果。

另外一个要求必须是能够以不需要编码新单元的方式序列化单元,而只是在XML文件中声明如下:

<DefinedUnits>
  <DirectUnits>
<!-- Base Units -->
<DirectUnit Symbol="kg"  Scale="1" Dims="(1,0,0,0,0)" />
<DirectUnit Symbol="m"   Scale="1" Dims="(0,1,0,0,0)" />
<DirectUnit Symbol="s"   Scale="1" Dims="(0,0,1,0,0)" />
...
<!-- Derived Units -->
<DirectUnit Symbol="N"   Scale="1" Dims="(1,1,-2,0,0)" />
<DirectUnit Symbol="R"   Scale="1.8" Dims="(0,0,0,0,1)" />
...
  </DirectUnits>
  <IndirectUnits>
<!-- Composite Units -->
<IndirectUnit Symbol="m/s"  Scale="1"     Lhs="m" Op="Divide" Rhs="s"/>
<IndirectUnit Symbol="km/h" Scale="1"     Lhs="km" Op="Divide" Rhs="hr"/>
...
<IndirectUnit Symbol="hp"   Scale="550.0" Lhs="lbf" Op="Multiply" Rhs="fps"/>
  </IndirectUnits>
</DefinedUnits>

答案 7 :(得分:1)

有jscience:http://jscience.org/,这里是单位的常规dsl:http://groovy.dzone.com/news/domain-specific-language-unit-。 iirc,c#有闭包,所以你应该能够搞砸了。

答案 8 :(得分:1)

您可以使用QuantitySystem而不是自己实现它。它建立在F#的基础上,大大改善了F#中的单元处理。这是我到目前为止发现的最好的实现,可以在C#项目中使用。

http://quantitysystem.codeplex.com

答案 9 :(得分:1)

<块引用>

值得使用吗?

是的。如果我面前有“一个数字”,我想知道那是什么。一天中的任何时间。此外,这就是我们通常所做的。我们将数据组织成一个有意义的实体 - 类、结构,你可以命名它。双精度为坐标,字符串为名称和地址等。为什么单位应该有所不同?

<块引用>

其他人已经做得更好了吗?

取决于人们如何定义“更好”。有一些图书馆,但我没有尝试过,所以我没有意见。此外它破坏了自己尝试的乐趣:)

现在关于实施。我想从显而易见的开始:尝试在 C# 中复制 F# 的 [<Measure>] 系统是徒劳的。为什么?因为一旦 F# 允许您直接在另一种类型上使用 / ^(或其他任何东西),游戏就会失败。祝你在 C# 中的 structclass 上好运。此类任务所需的元编程级别不存在,而且恐怕不会很快添加——在我看来。这就是为什么你缺少 Matthew Crumley 在他的回答中提到的维度分析。

让我们以 fsharpforfunandprofit.com 中的示例为例:您将 Newton 定义为 [<Measure>] type N = kg m/sec^2。现在您拥有了作者创建的 square 函数,该函数将返回一个 N^2,这听起来“错误”、荒谬且无用。 除非您想要执行算术运算,在评估过程中的某个时刻,您可能会得到一些“无意义”的东西,直到将其与其他单位相乘并得到有意义的结果。或者更糟糕的是,您可能想要使用常量。例如气体常数 R8.31446261815324 J /(K mol)。如果您定义了适当的单位,则 F# 已准备好使用 R 常量。 C# 不是。您需要为此指定另一种类型,但仍然无法对该常量执行任何操作。

这并不意味着您不应该尝试。我做到了,我对结果很满意。大约 3 年前,在我受到这个问题的启发后,我开始了 SharpConvert。触发是这样的故事:有一次我不得不为我开发的 RADAR simulator 修复一个令人讨厌的错误:一架飞机正在坠入地球,而不是遵循预定的下滑路径。正如您所猜测的那样,这并没有让我高兴,经过 2 小时的调试,我意识到在我的计算中,我将公里视为海里。在那之前,我就像“哦,好吧,我只会‘小心’”,这对于任何非平凡的任务来说至少是幼稚的。

在您的代码中,我会做一些不同的事情。

首先,我会将 UnitDouble<T>IUnit 实现转换为结构体。单位就是一个数字,如果您希望将它们视为数字,则结构体是更合适的方法。

然后我会避免方法中的 new T()。它不调用构造函数,它使用 Activator.CreateInstance<T>() 并且对于数字处理它会很糟糕,因为它会增加开销。这取决于实现,对于简单的单位转换器应用程序,它不会造成伤害。对于时间紧迫的上下文,避免像瘟疫一样。不要误会我,我自己使用了它,因为我不知道更好,前几天我运行了一些简单的基准测试,这样的调用可能会使执行时间加倍 - 至少在我的情况下。 Dissecting the new() constraint in C#: a perfect example of a leaky abstraction

中的更多详细信息

我也会更改 Convert<T, R>() 并使其成为成员函数。我更喜欢写作

var c = new Unit<Length.mm>(123);
var e = c.Convert<Length.m>();

而不是

var e = Length.Convert<Length.mm, Length.m>(c);

最后但并非最不重要的一点是,我将为每个物理量(长度时间等)使用特定的单位“壳”而不是 UnitDouble,因为添加物理量特定的函数和运算符重载会更容易。它还允许您创建一个 Speed<TLength, TTime> shell 而不是另一个 Unit<T1, T2> 甚至 Unit<T1, T2, T3> 类。所以它看起来像这样:

public readonly struct Length<T> where T : struct, ILength
{
    private static readonly double SiFactor = new T().ToSiFactor;
    public Length(double value)
    {
        if (value < 0) throw new ArgumentException(nameof(value));
        Value = value;
    }

    public double Value { get; }

    public static Length<T> operator +(Length<T> first, Length<T> second)
    {
        return new Length<T>(first.Value + second.Value);
    }

    public static Length<T> operator -(Length<T> first, Length<T> second)
    {
        // I don't know any application where negative length makes sense,
        // if it does feel free to remove Abs() and the exception in the constructor
        return new Length<T>(System.Math.Abs(first.Value - second.Value));
    }
    
    // You can add more like
    // public static Area<T> operator *(Length<T> x, Length<T> y)
    // or
    //public static Volume<T> operator *(Length<T> x, Length<T> y, Length<T> z)
    // etc

    public Length<R> To<R>() where R : struct, ILength
    {
        //notice how I got rid of the Activator invocations by moving them in a static field;
        //double mult = new T().ToSiFactor;
        //double div = new R().ToSiFactor;
        return new Length<R>(Value * SiFactor / Length<R>.SiFactor);
    }
}

另请注意,为了使我们免于可怕的 Activator 调用,我将 new T().ToSiFactor 的结果存储在 SiFactor 中。乍一看似乎很尴尬,但由于 Length 是通用的,Length<mm> 将拥有自己的副本,Length<Km> 拥有自己的副本,依此类推。请注意,ToSiFactor 是您方法的 toBase

我看到的问题是,只要您处于简单单位的领域并且达到时间的一阶导数,事情就很简单。如果您尝试做一些更复杂的事情,那么您会看到这种方法的缺点。打字

var accel = new Acceleration<m, s, s>(1.2);

不会像

那样清晰和“流畅”
let accel = 1.2<m/sec^2>

无论采用哪种方法,您都必须通过大量运算符重载来指定您需要的每个数学运算,而在 F# 中,您可以免费获得这些,即使结果没有意义,正如我在开头所写的那样。< /p>

此设计的最后一个缺点(或优势取决于您的看法)是它不能与单位无关。如果在某些情况下您需要“只是一个长度”,您就不能拥有它。您每次都需要知道您的长度是毫米、法定英里还是英尺。我在 SharpConvert 中采用了相反的方法,LengthUnit 派生自 UnitBase 和米公里等派生自此。这就是我不能顺着 struct 路径走的原因。这样你就可以:

LengthUnit l1 = new Meters(12);
LengthUnit l2 = new Feet(15.4);
LengthUnit sum = l1 + l2;

sum 将是米,但只要他们想在下一个操作中使用它就不必在意。如果他们想显示它,那么他们可以调用 sum.To<Kilometers>() 或任何单位。老实说,我不知道不将变量“锁定”到特定单元有什么好处。在某些时候可能值得对其进行调查。

答案 10 :(得分:1)

我希望编译器尽可能地帮助我。所以也许你可以有一个 TypedInt,其中 T 包含实际单位。

    public struct TypedInt<T>
    {
        public int Value { get; }

        public TypedInt(int value) => Value = value;

        public static TypedInt<T> operator -(TypedInt<T> a, TypedInt<T> b) => new TypedInt<T>(a.Value - b.Value);
        public static TypedInt<T> operator +(TypedInt<T> a, TypedInt<T> b) => new TypedInt<T>(a.Value + b.Value);
        public static TypedInt<T> operator *(int a, TypedInt<T> b) => new TypedInt<T>(a * b.Value);
        public static TypedInt<T> operator *(TypedInt<T> a, int b) => new TypedInt<T>(a.Value * b);
        public static TypedInt<T> operator /(TypedInt<T> a, int b) => new TypedInt<T>(a.Value / b);

        // todo: m² or m/s
        // todo: more than just ints
        // todo: other operations
        public override string ToString() => $"{Value} {typeof(T).Name}";
    }

您可以使用扩展方法来设置类型(或只是新的):

    public static class TypedInt
    {
        public static TypedInt<T> Of<T>(this int value) => new TypedInt<T>(value);
    }

实际单位可以是任何东西。这样,系统是可扩展的。 (处理转化的方式有多种。您认为哪种方式最好?)

    public class Mile
    {
        // todo: conversion from mile to/from meter
        // maybe define an interface like ITypedConvertible<Meter>
        // conversion probably needs reflection, but there may be
        // a faster way
    };

    public class Second
    {
    }

这样,你可以使用:

            var distance1 = 10.Of<Mile>();
            var distance2 = 15.Of<Mile>();
            var timespan1 = 4.Of<Second>();

            Console.WriteLine(distance1 + distance2);
            //Console.WriteLine(distance1 + 5); // this will be blocked by the compiler
            //Console.WriteLine(distance1 + timespan1); // this will be blocked by the compiler
            Console.WriteLine(3 * distance1);
            Console.WriteLine(distance1 / 3);
            //Console.WriteLine(distance1 / timespan1); // todo!

答案 11 :(得分:0)

为什么不使用CodeDom自动生成所有可能的单位排列?我知道这不是最好的 - 但我肯定会工作!

答案 12 :(得分:0)

参见Boo Ometa(可用于Boo 1.0): Boo Ometa and Extensible Parsing

答案 13 :(得分:0)

我真的很喜欢阅读这个堆栈溢出问题及其答案。

我有一个宠物项目,多年来我一直在修补,最近开始重新编写它并在http://ngenericdimensions.codeplex.com

将其发布到开源

这恰好与本页问答中表达的许多想法有些相似。

它主要是关于创建通用维度,使用度量单位和本机数据类型作为通用类型占位符。

例如:

Dim myLength1 as New Length(of Miles, Int16)(123)

还可以选择使用扩展方法,如:

Dim myLength2 = 123.miles

Dim myLength3 = myLength1 + myLength2
Dim myArea1 = myLength1 * myLength2

这不会编译:

Dim myValue = 123.miles + 234.kilograms

可以在自己的库中扩展新单元。

这些数据类型是仅包含1个内部成员变量的结构,使它们变得轻量级。

基本上,运算符重载仅限于“维”结构,因此每个度量单位都不需要运算符重载。

当然,一个很大的缺点是需要3种数据类型的泛型语法的声明时间越长。因此,如果这对您来说是个问题,那么这不是您的库。

主要目的是能够以编译时检查方式装饰具有单元的接口。

图书馆需要做很多事情,但我想发布它以防万一有人在寻找它。