C# 中的度量单位 - 几乎
受到 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#)
Inspired by Units of Measure in F#, and despite asserting (here) that you couldn't do it in C#, I had an idea the other day which I've been playing around with.
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
}
}
Example usage:
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
The next step is trying to implement conversions (snippet):
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);
}
}
(I would have liked to avoid instantiating objects by using static, but as we all know you can't declare a static method in an interface)
You can then do this:
var e = Length.Convert<Length.mm, Length.m>(c);
var f = Length.Convert<Length.mm, Mass.kg>(d); <-- but not this
Obviously, there is a gaping hole in this, compared to F# Units of measure (I'll let you work it out).
Oh, the question is: what do you think of this? Is it worth using? Has someone else already done better?
UPDATE for people interested in this subject area, here is a link to a paper from 1997 discussing a different kind of solution (not specifically for C#)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(14)
您缺少维度分析。 例如(从您链接到的答案),在 F# 中您可以这样做:
它将生成一个新的加速度单位,源自米和秒(您实际上可以使用模板在 C++ 中执行相同的操作)。
在 C# 中,可以在运行时进行维度分析,但它会增加开销,并且不会给您带来编译时检查的好处。 据我所知,没有办法在 C# 中完成完整的编译时单元。
当然,是否值得做取决于应用程序,但对于许多科学应用程序来说,这绝对是一个好主意。 我不知道任何现有的 .NET 库,但它们可能存在。
如果您对如何在运行时执行此操作感兴趣,其想法是每个值都有一个标量值和代表每个基本单位的幂的整数。
如果您不允许用户更改单位的值(无论如何都是一个好主意),您可以为常见单位添加子类:
您还可以在派生类型上定义更具体的运算符,以避免检查常见类型上的兼容单位。
You are missing dimensional analysis. For example (from the answer you linked to), in F# you can do this:
and it will generate a new unit of acceleration, derived from meters and seconds (you can actually do the same thing in C++ using templates).
In C#, it is possible to do dimensional analysis at runtime, but it adds overhead and doesn't give you the benefit of compile-time checking. As far as I know there's no way to do full compile-time units in C#.
Whether it's worth doing depends on the application of course, but for many scientific applications, it's definitely a good idea. I don't know of any existing libraries for .NET, but they probably exist.
If you are interested in how to do it at runtime, the idea is that each value has a scalar value and integers representing the power of each basic unit.
If you don't allow the user to change the value of the units (a good idea anyways), you could add subclasses for common units:
You could also define more specific operators on the derived types to avoid checking for compatible units on common types.
对同一度量的不同单位(例如,厘米、毫米和英尺表示长度)使用不同的类似乎有点奇怪。 基于 .NET Framework 的 DateTime 和 TimeSpan 类,我期望如下所示:
Using separate classes for different units of the same measure (e.g., cm, mm, and ft for Length) seems kind of weird. Based on the .NET Framework's DateTime and TimeSpan classes, I would expect something like this:
您可以在数字类型上添加扩展方法来生成度量。 它感觉有点像 DSL:
它并不是真正的 .NET 约定,也可能不是最容易发现的功能,因此也许您可以将它们添加到专门的命名空间中,供喜欢它们的人使用,并提供更传统的构造方法。
You could add extension methods on numeric types to generate measures. It'd feel a bit DSL-like:
It's not really .NET convention and might not be the most discoverable feature, so perhaps you'd add them in a devoted namespace for people who like them, as well as offering more conventional construction methods.
我最近在 GitHub 和 NuGet.
它为您提供所有常用单位和转换。 它是轻量级的,经过单元测试并支持 PCL。
转换示例:
I recently released Units.NET on GitHub and on NuGet.
It gives you all the common units and conversions. It is light-weight, unit tested and supports PCL.
Example conversions:
现在存在这样一个 C# 库:
http://www.codeproject.com/Articles/413750 /Units-of-Measure-Validator-for-Csharp
它具有与 F# 的单位编译时验证几乎相同的功能,但适用于 C#。
核心是 MSBuild 任务,它解析代码并寻找验证。
单元信息存储在注释和属性中。
Now such a C# library exists:
http://www.codeproject.com/Articles/413750/Units-of-Measure-Validator-for-Csharp
It has almost the same features as F#'s unit compile time validation, but for C#.
The core is a MSBuild task, which parses the code and looking for validations.
The unit information are stored in comments and attributes.
这是我对在 C#/VB 中创建单元的关注。 如果您认为我错了,请纠正我。 我读过的大多数实现似乎都涉及创建一个将值(int 或 double)与单位组合在一起的结构。 然后,您尝试为这些结构定义基本函数(+-*/等),并考虑单位转换和一致性。
我发现这个想法非常有吸引力,但每次我都会因为这对于一个项目来说是多么巨大的一步而犹豫不决。 这看起来像是一笔要么全有要么全无的交易。 您可能不会只是将一些数字更改为单位;而是将其更改为单位。 重点是项目内的所有数据都适当地用单位标记以避免任何歧义。 这意味着告别使用普通的双精度和整数,每个变量现在都被定义为“单位”或“长度”或“米”等。人们真的大规模这样做吗? 因此,即使你有一个很大的数组,每个元素也应该用一个单位标记。 这显然会对尺寸和性能产生影响。
尽管在尝试将单元逻辑推入后台方面很聪明,但对于 C# 来说,一些繁琐的符号似乎是不可避免的。 F# 做了一些幕后魔法,可以更好地减少单元逻辑的烦恼因素。
另外,当我们愿意时,如果不使用 CType 或“.Value”或任何其他符号,我们如何成功地让编译器像普通双精度一样对待一个单元? 例如对于可空值,代码知道如何处理双精度值吗? 就像双精度数一样(当然,如果你的双精度数?为空,那么你会得到一个错误)。
Here's my concern with creating units in C#/VB. Please correct me if you think I'm wrong. Most implementations I've read about seem to involve creating a structure that pieces together a value (int or double) with a unit. Then you try to define basic functions (+-*/,etc) for these structures that take into account unit conversions and consistency.
I find the idea very attractive, but every time I balk at what a huge step for a project this appears to be. It looks like an all-or-nothing deal. You probably wouldn't just change a few numbers into units; the whole point is that all data inside a project is appropriately labeled with a unit to avoid any ambiguity. This means saying goodbye to using ordinary doubles and ints, every variable is now defined as a "Unit" or "Length" or "Meters", etc. Do people really do this on a large scale? So even if you have a large array, every element should be marked with a unit. This will obviously have both size and performance ramifications.
Despite all the cleverness in trying to push the unit logic into the background, some cumbersome notation seems inevitable with C#. F# does some behind-the-scenes magic that better reduces the annoyance factor of the unit logic.
Also, how successfully can we make the compiler treat a unit just like an ordinary double when we so desire, w/o using CType or ".Value" or any additional notation? Such as with nullables, the code knows to treat a double? just like a double (of course if your double? is null then you get an error).
谢谢你的主意。 我在 C# 中以多种不同的方式实现了单元,但似乎总是有一个问题。 现在我可以使用上面讨论的想法再尝试一次。 我的目标是能够根据现有单位定义新单位,
并让程序找出合适的尺寸、比例和符号来使用。 最后,我需要构建一个基本代数系统,可以转换诸如
(m/s)*(m*s)=m^2
之类的内容,并尝试根据定义的现有单位来表达结果。此外,还必须能够以新单元不需要编码而只需在 XML 文件中声明的方式序列化单元,如下所示:
Thanks for the idea. I have implemented units in C# many different ways there always seems to be a catch. Now I can try one more time using the ideas discussed above. My goal is to be able to define new units based on existing ones like
and for the program to figure out the proper dimensions, scaling and symbol to use. In the end I need to build a basic algebra system that can convert things like
(m/s)*(m*s)=m^2
and try to express the result based on existing units defined.Also a requirement must be to be able to serialize the units in a way that new units do not need to be coded, but just declared in a XML file like this:
有jscience:http://jscience.org/,这里是单位的groovy dsl:http://groovy.dzone.com/news/domain-specific-language-unit- 。 iirc,c# 有闭包,所以你应该能够拼凑一些东西。
there is jscience: http://jscience.org/, and here is a groovy dsl for units: http://groovy.dzone.com/news/domain-specific-language-unit-. iirc, c# has closures, so you should be able to cobble something up.
为什么不使用 CodeDom 自动生成所有可能的单位排列? 我知道这不是最好的 - 但我一定会努力!
Why not use CodeDom to generate all possible permutations of the units automatically? I know it's not the best - but I will definitely work!
您可以使用 QuantitySystem 而不是自己实现。 它基于 F# 构建,并极大地改进了 F# 中的单元处理。 这是迄今为止我发现的最好的实现,可以在 C# 项目中使用。
http://quantitysystem.codeplex.com
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
是。 如果我面前有“一个数字”,我想知道那是什么。 一天中的任何时间。 此外,这就是我们通常所做的。 我们将数据组织成有意义的实体——类、结构,凡是你能想到的。 双倍转换为坐标,字符串转换为名称和地址等。为什么单位应该有所不同?
取决于人们如何定义“更好”。 有一些图书馆,但我没有尝试过,所以我没有意见。 除此之外,它破坏了我自己尝试的乐趣:)
现在谈谈实现。 我想从显而易见的事情开始:尝试在 C# 中复制 F# 的
[]
系统是徒劳的。 为什么? 因为一旦 F# 允许您直接在另一种类型上使用/
^
(或任何其他相关内容),游戏就会失败。 祝你好运,在 C# 中的struct
或class
上执行此操作。 在我看来,此类任务所需的元编程水平并不存在,而且恐怕不会很快添加。 这就是为什么你缺乏马修·克拉姆利在他的回答中提到的维度分析。让我们以 fsharpforfunandprofit.com 为例:您有
Newton 定义为
[] 类型 N = kg m/sec^2
。 现在您有了作者创建的square
函数,它将返回一个N^2
,这听起来“错误”、荒谬且无用。 除非您想要执行算术运算,在评估过程中的某个时刻,您可能会得到一些“无意义”的结果,直到您将其与其他单位相乘并得到有意义的结果。 或者更糟糕的是,您可能想使用常量。 例如,气体常数R
为8.31446261815324
J /(K mol)
。 如果您定义了适当的单位,那么 F# 就可以使用 R 常量。 C# 则不然。 您需要为此指定另一种类型,但您仍然无法对该常量执行任何您想要的操作。这并不意味着您不应该尝试。 我做到了,并且对结果非常满意。 大约 3 年前,在我受到这个问题的启发后,我开始了 SharpConvert 。 触发因素是这个故事:有一次我必须修复我开发的雷达模拟器的一个严重错误:一架飞机正在坠落在地球上而不是遵循预定的滑翔路径。 正如你可以猜到的那样,这并没有让我高兴,经过 2 个小时的调试,我意识到在我的计算中的某个地方,我将公里视为海里。 在那之前,我就像“哦,好吧,我会‘小心’”,这对于任何不平凡的任务来说至少是天真的。
在您的代码中,我会做一些不同的事情。
首先,我会将
UnitDouble
和IUnit
实现转换为结构。 单位就是一个数字,如果您希望将它们像数字一样对待,那么结构是更合适的方法。然后我会避免方法中的() 并且对于数字运算来说它会很糟糕,因为它会增加开销。 尽管这取决于实现,但对于简单的单位转换器应用程序来说,它不会造成损害。 对于时间紧迫的情况,请像瘟疫一样避免。 不要误会我的意思,我自己使用它,因为我不太了解,前几天我运行了一些简单的基准测试,这样的调用可能会使执行时间加倍 - 至少在我的情况下。 更多详细信息请参见 剖析 C# 中的 new() 约束:泄漏抽象的完美示例
new T()
。 它不调用构造函数,它使用 Activator.CreateInstance我还会更改
Convert()
并使其成为成员函数。 我更喜欢写作而不是
Last,但并非最不重要的是,我会为每个物理量(长度时间等)使用特定的单位“shell”而不是 UnitDouble,因为添加物理量特定函数和运算符重载会更容易。 它还允许您创建一个
Speed
shell,而不是另一个Unit
甚至Unit;
类。 所以它看起来像这样:另请注意,为了使我们免于可怕的 Activator 调用,我将 new T().ToSiFactor 的结果存储在 SiFactor 中。 一开始可能看起来很尴尬,但由于 Length 是通用的,
Length
将有自己的副本,Length
有自己的副本,依此类推。 请注意,ToSiFactor
是您方法的toBase
。我看到的问题是,只要你处于简单单位的领域并且达到时间的一阶导数,事情就很简单。 如果您尝试做一些更复杂的事情,那么您就会看到这种方法的缺点。 键入
不会像下面那样清晰和“流畅”
,而且无论采用哪种方法,您都必须通过大量运算符重载来指定所需的每个数学运算,而在 F# 中,您可以免费获得此操作,即使结果没有意义,如我一开始就在写。
这种设计的最后一个缺点(或优点取决于您如何看待它)是它不能与单位无关。 如果在某些情况下你需要“只是一个长度”,你就无法拥有它。 您每次都需要知道您的长度是毫米、法定英里还是英尺。 我在 SharpConvert 中采取了相反的方法,
LengthUnit
派生自UnitBase
,米公里等派生自此。 这就是为什么我不能顺着struct
路径走下去。 这样你就可以得到:sum
将是米,但只要他们想在下一个操作中使用它,就不应该关心。 如果他们想显示它,那么他们可以调用 sum.ToYes. If I have "a number" in front of me, I want to know what that is. Any time of the day. Besides, that's what we usually do. We organize data into a meaningful entity -class, struct, you name it. Doubles into coordinates, strings into names and address etc. Why units should be any different?
Depends on how one defines "better". There are some libraries out there but I haven't tried them so I don't have an opinion. Besides it spoils the fun of trying it myself :)
Now about the implementation. I would like to start with the obvious: it's futile to try replicate the
[<Measure>]
system of F# in C#. Why? Because once F# allows you to use/
^
(or anything else for that matter) directly on another type, the game is lost. Good luck doing that in C# on astruct
orclass
. The level of metaprogramming required for such a task does not exist and I'm afraid it is not going to be added any time soon -in my opinion. That's why you lack the dimensional analysis that Matthew Crumley mentioned in his answer.Let's take the example from fsharpforfunandprofit.com: you have
Newton
s defined as[<Measure>] type N = kg m/sec^2
. Now you have thesquare
function that that the author created that will return aN^2
which sounds "wrong", absurd and useless. Unless you want to perform arithmetic operations where at some point during the evaluation process, you might get something "meaningless" until you multiply it with some other unit and you get a meaningful result. Or even worse, you might want to use constants. For example the gas constantR
which is8.31446261815324
J /(K mol)
. If you define the appropriate units, then F# is ready to consume the R constant. C# is not. You need to specify another type just for that and still you won't be able to do any operation you want on that constant.That doesn't mean that you shouldn't try. I did and I am quite happy with the results. I started SharpConvert around 3 years ago, after I got inspired by this very question. The trigger was this story: once I had to fix a nasty bug for the RADAR simulator that I develop: an aircraft was plunging in the earth instead of following the predefined glide path. That didn't make me happy as you could guess and after 2 hours of debugging, I realized that somewhere in my calculations, I was treating kilometers as nautical miles. Until that point I was like "oh well I will just be 'careful'" which is at least naive for any non trivial task.
In your code there would be a couple of things I would do different.
First I would turn
UnitDouble<T>
andIUnit
implementations into structs. A unit is just that, a number and if you want them to be treated like numbers, a struct is a more appropriate approach.Then I would avoid the
new T()
in the methods. It does not invoke the constructor, it usesActivator.CreateInstance<T>()
and for number crunching it will be bad as it will add overhead. That depends though on the implementation, for a simple units converter application it won't harm. For time critical context avoid like the plague. And don't take me wrong, I used it myself as I didn't know better and I run some simple benchmarks the other day and such a call might double the execution time -at least in my case. More details in Dissecting the new() constraint in C#: a perfect example of a leaky abstractionI would also change
Convert<T, R>()
and make it a member function. I prefer writingrather than
Last but not least I would use specific unit "shells" for each physical quantity (length time etc) instead of the UnitDouble, as it will be easier to add physical quantity specific functions and operator overloads. It will also allow you to create a
Speed<TLength, TTime>
shell instead of anotherUnit<T1, T2>
or evenUnit<T1, T2, T3>
class. So it would look like that:Notice also that, in order to save us from the dreaded Activator call, I stored the result of
new T().ToSiFactor
in SiFactor. It might seem awkward at first, but as Length is generic,Length<mm>
will have its own copy,Length<Km>
its own, and so on and so forth. Please note thatToSiFactor
is thetoBase
of your approach.The problem that I see is that as long as you are in the realm of simple units and up to the first derivative of time, things are simple. If you try to do something more complex, then you can see the drawbacks of this approach. Typing
will not be as clear and "smooth" as
And regardless of the approach, you will have to specify every math operation you will need with hefty operator overloading, while in F# you have this for free, even if the results are not meaningful as I was writing at the beginning.
The last drawback (or advantage depending on how you see it) of this design, is that it can't be unit agnostic. If there are cases that you need "just a Length" you can't have it. You need to know each time if your Length is millimeters, statute mile or foot. I took the opposite approach in SharpConvert and
LengthUnit
derives fromUnitBase
and Meters Kilometers etc derive from this. That's why I couldn't go down thestruct
path by the way. That way you can have:sum
will be meters but one shouldn't care as long as they want to use it in the next operation. If they want to display it, then they can callsum.To<Kilometers>()
or whatever unit. To be honest, I don't know if not "locking" the variable to a specific unit has any advantages. It might worth investigating it at some point.我希望编译器能够尽可能地帮助我。 所以也许你可以有一个 TypedInt,其中 T 包含实际单位。
您可以使用扩展方法来设置类型(或只是新的):
实际单位可以是任何单位。 这样,系统就具有可扩展性。
(处理转换的方法有多种。您认为哪种方法最好?)
这样,您可以使用:
I would like the compiler to help me as much as possible. So maybe you could have a TypedInt where T contains the actual unit.
You could have an extensiom method to set the type (or just new):
The actual units can be anything. That way, the system is extensible.
(There's multiple ways of handling conversions. What do you think is best?)
This way, you can use:
请参阅 Boo Ometa(适用于 Boo 1.0):
Boo Ometa 和可扩展解析
See Boo Ometa (which will be available for Boo 1.0):
Boo Ometa and Extensible Parsing
我真的很喜欢阅读这个堆栈溢出问题及其答案。
我有一个多年来一直在修改的宠物项目,最近开始重写它,并将其发布到开源 https://github.com/MafuJosh/NGenericDimensions
它恰好与本页的问题和答案中表达的许多想法有些相似。
它基本上是关于创建通用维度,使用度量单位和本机数据类型作为通用类型占位符。
例如:
还可以选择使用一些扩展方法,例如:
这
不会编译:
新单元可以在您自己的库中扩展。
这些数据类型是仅包含 1 个内部成员变量的结构,因此它们是轻量级的。
基本上,运算符重载仅限于“维度”结构,因此每个度量单位都不需要运算符重载。
当然,一个很大的缺点是泛型语法的声明较长,需要 3 种数据类型。 因此,如果这对您来说是个问题,那么这不是您的图书馆。
主要目的是能够以编译时检查方式用单元装饰界面。
图书馆还有很多工作要做,但我想将其发布,以防有人正在寻找这种东西。
I really liked reading through this stack overflow question and its answers.
I have a pet project that I've tinkered with over the years, and have recently started re-writing it and have released it to the open source at https://github.com/MafuJosh/NGenericDimensions
It happens to be somewhat similar to many of the ideas expressed in the question and answers of this page.
It basically is about creating generic dimensions, with the unit of measure and the native datatype as the generic type placeholders.
For example:
With also some optional use of Extension Methods like:
And
This would not compile:
New units can be extended in your own libraries.
These datatypes are structures that contain only 1 internal member variable, making them lightweight.
Basically, the operator overloads are restricted to the "dimension" structures, so that every unit of measure doesn't need operator overloads.
Of course, a big downside is the longer declaration of the generics syntax that requires 3 datatypes. So if that is a problem for you, then this isn't your library.
The main purpose was to be able to decorate an interface with units in a compile-time checking fashion.
There is a lot that needs to be done to the library, but I wanted to post it in case it was the kind of thing someone was looking for.