【问题标题】:Units of measure in C# - almostC# 中的度量单位 - 几乎
【发布时间】:2010-09-25 19:39:18
【问题描述】:

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

下一步是尝试实现转换(sn-p):

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#)

【问题讨论】:

  • 看看 Frink 计算器和 Frink 编程语言。
  • 我想知道是否有人在 C# 中使用类属性值的属性来处理单元。
  • Frink 是这类问题的炸弹。
  • 我可能在这里遗漏了一些明显的东西,但是您为什么想要/需要另一个(即 F# 以外的)基于 CLR 的度量单位实现?还是只是为了“这样做”?
  • @pblasucci,只是为了它。 (只是为了挑剔,F# 实现不是基于 CLR,这一切都发生在编译器中,运行时什么都看不到)...

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


【解决方案1】:

对相同度量的不同单位使用不同的类(例如,长度为厘米、毫米和英尺)似乎有点奇怪。基于 .NET Framework 的 DateTime 和 TimeSpan 类,我希望是这样的:

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

【讨论】:

  • 这也是我的第一直觉。这样做的缺点是您必须明确定义组合所有单位排列的运算符。当您开始将不同的单位(例如速度(长度/时间跨度))组合在一起时,这会变得更加复杂,您需要支持大量的 FromXXX 转换。
【解决方案2】:

有 jscience:http://jscience.org/,这是一个 groovy dsl for units:http://groovy.dzone.com/news/domain-specific-language-unit-。 iirc,c# 有闭包,所以你应该能够拼凑一些东西。

【讨论】:

    【解决方案3】:

    您缺少维度分析。例如(根据您链接到的答案),在 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
        {
        }
    }
    

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

    【讨论】:

    • 是的,我知道这是缺少的,我非常喜欢您的解决方案,但正如您所说,这不是编译时间。无论如何都要投票。
    • 我不喜欢看到随着我们添加更多基本单元而变得复杂的初始化程序。由于您已经失去了在编译时检测错误单位的能力,您可以更进一步,只需使用将字符串或枚举映射到 int 的字典,而不是为每种类型使用单独的字段。
    • 国际单位制只有7个基本单位(时间、质量、长度、温度、发光强度、物质数量和电流)。如果您将乘数值添加到 Unit,它是转换工厂返回 SI 表示,您可以获得一个相当好的模型。
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

      参见 Boo Ometa(将在 Boo 1.0 中可用): Boo Ometa and Extensible Parsing

      【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

        【解决方案7】:

        感谢您的想法。我用 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>
        

        【讨论】:

        • 不知道为什么你被否决了,但我认为你最好学习 F#,而不是尝试重新发明轮子。我更新了我的问题,提供了您可能感兴趣的论文的链接。
        • 感谢您的建设性意见。
        【解决方案8】:

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

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

        尽管在尝试将单元逻辑推到后台时很聪明,但 C# 似乎不可避免地会出现一些繁琐的符号。 F# 做了一些幕后魔术,更好地减少了单元逻辑的烦恼因素。

        另外,如果我们需要,我们如何使编译器像普通双精度一样处理一个单元,而不使用 CType 或“.Value”或任何其他符号?比如使用 nullables,代码知道处理 double 吗?就像一个双倍(当然,如果你的双倍?为空,那么你会得到一个错误)。

        【讨论】:

        • 是的,稍后再讨论,但这就是为什么我认为这最好是帮助开发人员而不是代码库的核心语言功能,这样编译后的代码中就没有抽象了本身。
        【解决方案9】:

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

        我有一个多年来一直在修补的宠物项目,最近开始重新编写它,并将其发布到https://github.com/MafuJosh/NGenericDimensions的开源网站

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

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

        例如:

        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 种数据类型的泛型语法声明较长。因此,如果这对您来说是个问题,那么这不是您的图书馆。

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

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

        【讨论】:

          【解决方案10】:

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

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

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

          【讨论】:

          【解决方案11】:

          我最近在 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
          

          【讨论】:

            【解决方案12】:

            您可以使用 QuantitySystem 而不是自己实现它。它建立在 F# 之上,并极大地改进了 F# 中的单元处理。这是迄今为止我发现的最好的实现,可以在 C# 项目中使用。

            http://quantitysystem.codeplex.com

            【讨论】:

              【解决方案13】:

              值得用吗?

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

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

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

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

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

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

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

              首先我会将UnitDouble&lt;T&gt;IUnit 实现转换为结构。一个单元就是一个数字,如果您希望它们被视为数字,结构是一种更合适的方法。

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

              我还会更改Convert&lt;T, R&gt;() 并使其成为成员函数。我更喜欢写作

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

              而不是

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

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

              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&lt;mm&gt; 将有自己的副本,Length&lt;Km&gt; 将有自己的副本,依此类推。请注意,ToSiFactor 是您的方法的 toBase

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

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

              不会像

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

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

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

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

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

              【讨论】:

              • 哇!这是一次记忆之旅。感谢您提供有趣的反馈。我最近遇到了一个类似的问题,我用类型解决了(不是很优雅) - 来自具有不同坐标系的源的数据的矢量计算 - 我最终得到了 PointXY、PointYZ 和 PointXZ。不漂亮,但发现了几个错误。
              • @Benjol 哈哈,我希望那条车道有美好的回忆:) 确实它可能更漂亮,但如果它已经暴露了一些错误,那就足够了。
              【解决方案14】:

              我希望编译器尽可能地帮助我。所以也许你可以有一个 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!
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-09-22
                • 1970-01-01
                • 2015-10-03
                • 2012-09-22
                • 2011-05-07
                • 1970-01-01
                相关资源
                最近更新 更多