63

受到F# 中的度量单位的启发,尽管断言(在这里)你不能在 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);
    }
}

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

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

显然,与 F# 度量单位相比,这有一个巨大的漏洞(我会让你算出来)。

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

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

4

14 回答 14

42

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

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

于 2008-12-08T08:03:59.930 回答
19

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

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

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

于 2010-06-01T12:43:19.390 回答
18

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

Length  length       = Length.FromMillimeters(n1);
decimal lengthInFeet = length.Feet;
Length  length2      = length.AddFeet(n2);
Length  length3      = length + Length.FromMeters(n3);
于 2008-12-08T07:46:23.243 回答
14

我最近在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
于 2013-07-21T22:55:06.920 回答
11

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

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

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

于 2012-07-03T15:05:53.573 回答
4

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

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

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

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

于 2011-02-09T21:54:20.620 回答
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>
于 2010-11-16T20:13:29.177 回答
1

you could use QuantitySystem instead of implementing it by your own. It builds on F# and drastically improves unit handling in F#. It's the best implementation I found so far and can be used in C# projects.

http://quantitysystem.codeplex.com

于 2014-03-30T20:21:21.303 回答
1

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

于 2008-12-08T07:55:29.637 回答
1

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

于 2008-12-08T08:31:54.087 回答
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!
于 2021-05-18T13:42:03.547 回答
1

值得使用吗?

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

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

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

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

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

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

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

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

然后我会避免new T()方法中的。它不调用构造函数,它使用Activator.CreateInstance<T>()并用于数字运算,因为它会增加开销。尽管这取决于实现,但对于简单的单位转换器应用程序来说,它不会造成伤害。对于时间紧迫的上下文,避免像瘟疫一样。不要误会我的意思,我自己使用它,因为我不知道更好,前几天我运行了一些简单的基准测试,这样的调用可能会使执行时间加倍 - 至少在我的情况下。剖析 C#中的 new() 约束中的更多详细信息:泄漏抽象的完美示例

我也会更改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>它自己的,等等等等。请注意,这ToSiFactortoBase您的方法。

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

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

不会像

let accel = 1.2<m/sec^2>

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

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

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

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

于 2021-05-12T21:11:51.160 回答
0

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

我有一个多年来一直在修补的宠物项目,最近开始重新编写它并将其发布到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 种数据类型的泛型语法声明较长。因此,如果这对您来说是个问题,那么这不是您的图书馆。

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

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

于 2011-09-27T04:05:27.397 回答
0

请参阅 Boo Ometa(可用于 Boo 1.0): Boo Ometa 和可扩展解析

于 2009-01-28T22:07:18.317 回答