关于f#:C#中的度量单位

Units of measure in C# - almost

受F_中度量单位的启发,尽管(这里)断言在C_中无法做到这一点,但前几天我有了一个想法,我一直在玩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
    }
}

示例用法:

1
2
3
4
5
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

下一步是尝试实现转换(代码段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
    }
}

(我希望避免使用static实例化对象,但众所周知,您不能在接口中声明静态方法)然后您可以这样做:

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

显然,与f测量单位相比,这里有一个缺口(我会让你计算出来)。

哦,问题是:你觉得这个怎么样?它值得使用吗?其他人做得更好了吗?

对这个主题感兴趣的人的更新,这里有一个链接,指向一篇1997年的论文,讨论一种不同的解决方案(不是专门针对C)


您缺少维度分析。例如(从你链接到的答案),在f中,你可以这样做:

1
let g = 9.8<m/s^2>

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

在C中,可以在运行时进行维度分析,但这会增加开销,并且不会给编译时检查带来好处。据我所知,没有办法用C来完成完整的编译时间单位。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
            ...;
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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
    {
    }
}

还可以在派生类型上定义更具体的运算符,以避免检查公共类型上的兼容单元。


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

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

它不是真正的.NET约定,也可能不是最容易发现的功能,所以您可能会将它们添加到一个专门的名称空间中,供喜欢它们的人使用,并提供更传统的构造方法。


对相同度量单位(例如,长度为cm、mm和ft)使用不同的类似乎有点奇怪。基于.NET框架的日期时间和TimeSpan类,我期望如下所示:

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


我最近在Github和Nuget上发布了Units.net。

它提供了所有常用的单位和转换。它重量轻,单元测试,支持PCL。

示例转换:

1
2
3
4
5
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

现在存在这样一个C库:http://www.codeproject.com/articles/413750/units-of-measure-validator-for-csharp

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

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


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

我觉得这个想法很有吸引力,但每次我犹豫在一个项目的巨大一步这似乎是。这看起来像是一个全无的交易。您可能不只是将一些数字更改为单位;关键是项目中的所有数据都适当地用单位标记,以避免任何模糊性。这就意味着不再使用普通的双精度和整数了,现在每个变量都被定义为"单位"、"长度"或"米"等。人们真的大规模地这样做吗?因此,即使您有一个大数组,每个元素都应该用一个单元来标记。这显然会对大小和性能产生影响。

尽管在尝试将单元逻辑推入后台时非常聪明,但是一些繁琐的符号似乎在C中是不可避免的。f做一些幕后魔术,更好地减少单元逻辑的烦人因素。

另外,当我们需要时,如何使编译器像对待普通的双精度数一样成功地处理一个单元,而不使用ctype或".value"或任何附加符号?例如对于nullables,代码知道如何处理double?就像一个双人间(当然,如果你的双人间?如果为空,则会出现错误)。


谢谢你的建议。我已经用C实现了很多不同的方法,似乎总是有一个陷阱。现在,我可以再次尝试使用上面讨论的想法。我的目标是能够根据现有的单位定义新的单位,例如

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

并为程序计算出合适的尺寸、比例和符号使用。最后,我需要建立一个基本的代数系统,它可以转换像(m/s)*(m*s)=m^2这样的东西,并尝试根据现有的定义单位来表示结果。

另外,要求必须能够以不需要对新单元进行编码的方式序列化这些单元,而只需要在这样的XML文件中声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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>


您可以使用QuantitySystem而不是自己实现它。它建立在f_的基础上,大大提高了f_的单元处理能力。这是迄今为止我发现的最好的实现,可以在C项目中使用。

http://quantitysystem.codeplex.com


这里有jscience:http://jscience.org/,这里有一个用于单位的groovy DSL:http://groovy.dzone.com/news/domain-specific-language-unit-。iirc,c有闭包,所以你应该可以把一些东西拼凑起来。


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

我有一个多年来一直在修补的宠物项目,最近开始重新编写它,并在http://ngenericdimensions.codeplex.com上将其发布到开放源代码。

它恰巧与本页问答中表达的许多观点有些相似。

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

例如:

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

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

1
Dim myLength2 = 123.miles

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

这不会编译:

1
Dim myValue = 123.miles + 234.kilograms

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

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

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

当然,一个很大的缺点是需要3个数据类型的泛型语法声明的时间更长。所以如果这对你来说是个问题,那么这不是你的图书馆。

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

图书馆需要做很多工作,但我想把它贴出来,以防有人在找这种东西。


请参见Boo Omea(可用于Boo 1.0):booometa和可扩展解析


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