C# 单位验证器






4.66/5 (28投票s)
该库提供了一个 MSBuild 任务,用于在 C# 代码中对度量单位进行编译时验证。
引言
在 C# 中,1 米加上 1 秒的结果是 2。但在大多数情况下,将不同单位的两个量相加或赋值是程序员的错误,会导致意外且不期望的运行时行为。
此验证器在编译时检查单位违规,而无需添加新语法或更改运行时行为——只需通过注释或属性指定所使用的单位。
考虑到图 1 中的单位,planet.Speed
是米/秒,resultingForce
是牛顿 (kg * m/s^2),planet.Mass
是千克。由于牛顿除以千克得到的量纲是加速度,这个量不能赋值给速度——这是程序员的错误,但甚至在整个应用程序启动之前就被检测到了。
用法
该验证器以 MSBuild 任务的形式提供,可以包含在任何 C# 项目中,运行时无需引用。单位可以在单个项目中通过 XML 文档声明,但为了项目的整体一致性,建议使用 Unit
属性。在这种情况下,运行时需要引用 HDUnitsOfMeasureLibrary。
安装
首先,下载此库的二进制文件或自行编译该库。所有这些程序集都应放在一个合适的文件夹中。
如果您想为所有项目安装验证器(不推荐,且您应该知道自己在做什么),可以编辑 Microsoft.CSharp.Targets 文件,否则可以通过编辑其 csproj 文件为每个项目单独安装。为此,请用编辑器打开文件并替换以下注释:
<!--
<Target Name="BeforeBuild">
</Target>
-->
用此 XML 替换
<UsingTask TaskName="HDLibrary.UnitsOfMeasure.Validator.UnitValidationTask"
AssemblyFile="Path-To-Your-Installation-Folder\HDUnitsOfMeasureValidatorTask.dll" />
<Target Name="BeforeBuild" DependsOnTargets="ResolveProjectReferences;ResolveAssemblyReferences">
<UnitValidationTask Files="@(Compile)" References="@(_ResolveAssemblyReferenceResolvedFiles)" />
</Target>
您应该将“Path-To-Your-Installation-Folder”替换为您放置 HDUnitsOfMeasureValidatorTask.dll 副本的文件夹路径。
就是这样——下次构建项目时,单位违规将在 Visual Studio 中显示!
入门
默认情况下,每个变量或成员的单位都是未知的。未知单位的行为与没有验证时相同。因此,要使用此验证器,物理元素(如速度、长度或时间)应标记相应的单位。
只有对这些已标记元素的运算才会被评估。
声明单位
对于局部变量
您可以通过添加一个单行注释来声明局部变量的单位,该注释将单位括在方括号中。在注释标记(//)和开括号之间不允许有任何字符。闭括号之后,可以添加解释性文本,用空格分隔。
示例
int length; //[m] the length in meter
double time; //[s]
对于属性和字段
属性和字段的单位可以通过 XML 文档声明:
/// <summary>
/// The length in [m]
/// </summary>
private double length;
/// <summary>
/// The time in [s]
/// </summary>
private double Time {get; set;}
单位可以指定在 summary 标签中的任何位置,并且必须括在方括号中。
缺点是,单位描述仅对项目内的引用可用,因为来自其他项目的引用目前无法访问 XML 文档,并将该成员的单位视为“未知”。
在这种情况下,您可以使用 UnitAttribute
。在这种情况下,缺点是您必须添加对 HDUnitsOfMeasureLibrary.dll 的程序集引用,并且此依赖关系仍然存在于运行时。尽管如此,使用 Unit
属性是推荐的方式
[Unit("m")]
private double length;
[Unit("s")]
private double Time {get; set;}
对于方法、构造函数和参数
方法和构造函数是参数化成员,它们具有多个输入单位和一个或零个输出单位(对于构造函数,如果给出输出单位,它决定了创建对象的单位)。
如果您想在 XML 文档中声明单位,可以这样做:
/// <param name="time">time ([s])</param>
/// <param name="number">number in [1] - number is a dimensionless quantity</param>
/// <param name="unknown">can be any unit, no unit is specified</param>
/// <returns>the length, [m]</returns>
public int GetLength(int time, int number, double unknown) {...}
如果未为任何参数指定单位,则其单位为未知。
未知单位与无量纲单位的区别
如果未指定单位,则单位为未知。未知单位等同于无量纲单位,但不会验证分配给未知数量的值。
double number = 1; //[1] dimensionless unit
double value = 1; //unknown unit
double length = 1.Meter(); //[m] a length in meters
number = value; //is ok
value = number; //is ok
value = length; //is ok, since no validation is done
number = length; //is not ok, since number is "1" and length is "m"
单位的恶性循环
由于硬编码数字的单位是无量纲的,因此您不能将其分配给一个单位不是无量纲的元素。要打破这个循环,您可以在赋值中的“=”之后精确地插入注释“/*ignore unit*/”(无空格)——此类赋值不会被验证。
通过 Units
类中的预定义常量或 int
和 double
上的扩展方法,您可以在不使用“ignore unit”的情况下轻松地将任何未知单位或无量纲单位转换为任何目标单位。
int time = 5; //[m] -> not ok, since 5 is unknown ("1") and time is "m"
time =/*ignore unit*/5; -> is ok, since this assignment will not be validated
time = 5 * Units.Meter; //is ok, since Units.Meter is 1 meter
time = 5.Meter(); //is ok
time = 5.AsUnit("m"); //is ok
time = 5.AsUnit("km").ConvertFromTo("km", "m"); //is ok
验证
以下对具有已知单位的元素的赋值会被验证
=, *=, /=, +=, -=
如果参数需要特定单位,则在方法调用中的参数会得到验证。
验证成功,如果表达式推断出的单位等于目标元素的单位。两个单位相等,如果它们具有相同的相干单位(请参阅 此处,第“什么是相干单位?”部分)并且与它的转换因子相同。因此,“km”不等于“m”,但“MN”(兆牛顿)等于“Gg * m / s^2”(千兆克 * 米 / 平方秒)。
动态单位描述
通常,每个单位描述都是静态的。如果“a”被声明为米,那么“a”将始终是米。
但对于类成员或方法,情况并非如此:如果一个向量被声明为米,则向量的长度也是米。如果一个向量被声明为牛顿,则向量的长度将是牛顿。
这种泛化可以通过动态单位描述来实现。这些描述可以通过涉及使用的上下文(目标对象和参数)解析为具体单位。这种动态单位可以用 DynamicUnitAttribute
指定。
有三种动态表达式:只有“@”可以与属性和字段一起使用。如果您将向量的长度声明为“@”,则将插入目标对象的单位。所以,如果向量声明为 X,则其长度也是 X。您甚至可以构建更复杂的表达式,如“m/@”或“@^2”——根据上下文,这将是“m/m”或“m^2”(如果目标对象声明为“m”)。
使用花括号,可以引用大括号内参数的单位。方括号将插入传递给被引用参数的值。如果被引用参数是字符串并且具有 UnitDescriptionAttribute
属性,则验证器将检查此参数是否为有效的单位字符串,如果该参数是常量表达式。此属性可以为该字符串定义一个底层的单位约束。如果底层单位约束是“m”,则有效参数是“km”或“cm”。此约束也可以是动态的。
参数可以通过其零基索引或名称来引用。
例如,这里是 ConvertFromTo
方法的签名
[return: DynamicUnit("[targetUnitString]")]
public static double ConvertFromTo([DynamicUnit("[sourceUnitString]")] this double value,
[UnitString] string sourceUnitString, [UnitString(UnderlayingUnitConstraint = "[sourceUnitString]")] string targetUnitString)
结果单位是 targetUnitString
参数的值。targetUnitString
必须是有效的单位字符串,并且必须与 sourceUnitString
参数具有相同的底层单位。
如果值为公里,则 sourceUnitString
必须是“km”,否则验证器将记录错误。如果 sourceUnitString
是“km”,则 targetUnitString
也必须是某种长度单位——例如,“cm”或“µm”。
外部单位定义和描述
如果您想引入新单位,可以使用程序集属性 UnitDefinitionAttribute
:
[assembly: UnitDefinition("N", "Newton", "kg * m/s^2")]
此单位随后可用于此项目以及引用此项目的所有项目。
如果您想描述某个外部元素的单位,可以使用属性 UnitDescriptionAttribute
[assembly: UnitDescription("System.Windows.Vector.Length", ResultUnit = "@")]
除了结果单位,您还可以定义参数化成员的参数单位。在这种情况下,您甚至可以为此描述指定重载(例如,“double,double”)。
示例
[assembly: UnitDescription("System.Math.Min", ResultUnit = "{0}", ParameterUnits = ",{0}")]
此描述强制第二个参数具有与第一个参数相同的单位。同时,它指定了结果的单位。第一个参数可以是任何单位。
因此,Math.Min(1.Meter(), 2.Meter()) 的结果单位将是米。
实现简介
此验证器的核心库是 NRefactory 和我的 HDUnitsOfMeasureLibrary。
验证过程分为不同的步骤,其中一些是并行进行的。
首先,代码被解析为抽象语法树。并行地,使用 Mono.Cecil 加载(如果未缓存)引用的程序集。然后将两者转换为单个类型系统。遍历此类型系统以查找对同一元素的引用,并从属性和 XML 文档中提取单位。在此步骤之后,每个元素都有一个单位描述。单位描述可以通过分析传入的目标对象和参数来将可能的动态单位解析为具体单位。
下一步,再次遍历整个抽象语法树(但现在是并行进行的),以使用提取的单位和表达式的推断单位来验证赋值和方法调用。
未来
此库仍处于开发阶段——仍然存在一些错误和限制。
目前,一个大问题是性能:尽管进行了并行处理,但仍然相当慢——但仍有许多可以优化的地方。
历史
- 1.0 初始版本