Introduction
In C#, the sum of 1 meter and 1 second will be 2. But in most cases, adding or assigning two quantities of different units is a mistake of the programmer, which causes an unexpected and undesired runtime behavior.
This validator checks at compile time for unit violations without adding a new syntax or changing the runtime behavior - only the used units has to be specified through comments or attributes.
Figure 1: Validation Sample
Considering the units in figure 1, planet.Speed
is meter per second, resultingForce
is Newton (kg * m/s^2) and planet.Mass
is kilogram.
Since the quotient of Newton and kilogram is an acceleration, this quantity cannot be assigned to a speed - a mistake of the programmer, but detected even before starting the whole application.
Usage
The validator is available as MSBuild task and can be included inside any cs-project, whereas a reference at runtime is not needed. The units can be declared within a single project through xml documentation, but for project wide consistence, it is recommend to use the Unit
-attribute. In this case, a runtime reference is needed to HDUnitsOfMeasureLibrary.
Installation
First, download either the binaries of this library or compile the library by yourself. All these assemblies should be placed into a suitable folder.
If you want to install the validator for all projects (not recommended, and you should know what you are doing), you can edit the Microsoft.CSharp.Targets file, otherwise you can install it for each project separately by editing its csproj-file. To do this, open the file with an editor and replace the following comment:
<!---->
with this 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>
You should adapt "Path-To-Your-Installation-Folder" to the path of your folder in which you placed your copy of HDUnitsOfMeasureValidatorTask.dll.
That is it - the next time you will build your project, unit violations are displayed within Visual Studio!
First Steps
By default, the unit of each variable or member is unknown. The behavior of unknown units is the same as without validation.
Thus, to work with this validator, the physical elements (like speed, length or time) should be marked with their corresponding units.
Only operations on those marked elements are evaluated.
Declaring Units
For Local Variables
You can declare a unit for a local variable by adding an one-line comment which encloses the unit in square brackets. Between the comment token (//) and the opening bracket, no characters are allowed. After the closing bracket, an explaining text can be added, separated by a whitespace.
Examples:
int length; double time;
For Properties and Fields
The units of properties and fields can be declared through a xml documentation:
private double length;
private double Time {get; set;}
The unit can be specified anywhere inside the summary-tag and has to be enclosed in square brackets.
It is of disadvantage, that the unit description is only available for references within this project, because references from other projects currently have no access to the xml documentation and assume the unit of this member as "unknown".
For this case, you can use the UnitAttribute
. Here it is of disadvantage, that you have to add an assembly reference to HDUnitsOfMeasureLibrary.dll, and this dependency still exists at runtime. Nevertheless, using the Unit
-attribute is the recommended way:
[Unit("m")]
private double length;
[Unit("s")]
private double Time {get; set;}
For Methods, Constructors and Parameters
Methods and constructors are parameterized members, which have several input units and zero or one output unit (for constructors, the output unit, if given, determines the unit of the created object).
If you want to declare the unit within the xml documentation, you can do it the following way:
public int GetLength(int time, int number, double unknown) {...}
If no unit is specified for any of the parameters, its unit is unknown.
Unknown Units in contrast to Dimensionless Units
If no unit is specified, the unit is unknown. Unknown units are equal to dimensionless units, but assigns to unknown quantities are not validated:
double number = 1; double value = 1; double length = 1.Meter();
number = value; value = number; value = length; number = length;
The Vicious Circle of Units
Since the unit of a hard coded number is dimensionless, you cannot assign it to an element with a different unit than dimensionless. To break this circle, you can insert the comment "/*ignore unit*/" (no whitespaces) exactly after the "=" in the assignment - such assignments are not validated.
With the predefined constants in the Units
class or the extension methods on int
and double
, you can easily convert any unknown or dimensionless unit into any target unit without using "ignore unit".
int time = 5; time =5; -> is ok, since this assignment will not be validated
time = 5 * Units.Meter; time = 5.Meter(); time = 5.AsUnit("m"); time = 5.AsUnit("km").ConvertFromTo("km", "m");
Validation
The following assignments to elements with a known unit are validated:
=, *=, /=, +=, -=
Arguments in method invocations are validated, if the parameters require certain units.
A validation is successful, if the inferred unit from the expression is equal to the unit of the target element. Two units are equal if they have the same coherent unit (see here, chapter "What are coherent Units?") and the same conversion factor to it. So, "km" is not equal to "m", but "MN" (Mega newton) is equal to "Gg * m / s^2" (Giga gram * meter / square-second).
Dynamic Unit Descriptions
Usually, each unit description is static. If "a" is declared as meter, "a" will be always meter.
But this is not true for class members or methods: If a vector is declared as meter, the length of the vector is meter too. If a vector is declared as Newton, the length of the vector will be Newton.
This genericity can be accomplished by dynamic unit descriptions. Those descriptions can be resolved into concrete units by involving the used context (the target object and the arguments). Such a dynamic unit can be specified with the DynamicUnitAttribute
.
There are three kinds of dynamic expressions: The "@" is the only one which can be used with properties and fields. If you declare the length of a vector as "@", the unit of the target object will be inserted. So if the vector is declared as X, its length is X too. You can even build some more complex expressions like "m/@" or "@^2" - depending on the context it will be "m/m" or "m^2" (if the target object is declared as "m").
With curly brackets, the unit of the enclosed parameter can be referenced. Square brackets will insert the value of the argument passed to the referenced parameter. If the referenced parameter is a string and has the attribute UnitDescriptionAttribute
, the validator will check this argument to be a valid unit string, if the argument is a constant expression. This attribute can define an underlaying unit constraint for this string. If the underlaying unit constraint is "m", valid arguments are "km" or "cm". This constraint can be dynamic, too.
A parameter can be referenced by its zero based index or its name.
As example, here the signature of the ConvertFromTo
method:
[return: DynamicUnit("[targetUnitString]")]
public static double ConvertFromTo([DynamicUnit("[sourceUnitString]")] this double value,
[UnitString] string sourceUnitString, [UnitString(UnderlayingUnitConstraint = "[sourceUnitString]")] string targetUnitString)
The result unit is the value of the targetUnitString
argument. targetUnitString
has to be a valid unit string, which has to be the same
underlying unit as the sourceUnitString
argument.
If value is kilometer,
sourceUnitString
has to be "km", otherwise the validator logs an error. If
sourceUnitString
is "km", targetUnitString
has to be some kind of length too - e.g. "cm" or "µm".
External Unit Definition and Description
If you want to introduce a new unit, you can use the assembly attribute UnitDefinitionAttribute
:
[assembly: UnitDefinition("N", "Newton", "kg * m/s^2")]
This unit is then available for this project and all projects, which reference this project.
If you want to describe the units of an foreign element, you can use the attribute UnitDescriptionAttribute
:
[assembly: UnitDescription("System.Windows.Vector.Length", ResultUnit = "@")]
Beside the result unit, you can define the parameter units for parameterized members too. In this case, you can even specify the overloading
for this description (e.g. "double,double").
An example:
[assembly: UnitDescription("System.Math.Min", ResultUnit = "{0}", ParameterUnits = ",{0}")]
This description forces the second argument to have the same unit as the first. At the same time it specifies the unit of the result. The first argument can be any unit.
Thus, the resulting unit of Math.Min(1.Meter(), 2.Meter()) will be meter.
Implementation, a Short Overview
The core libraries of this validator are NRefactory and my HDUnitsOfMeasureLibrary.
The process of validation is separated into different steps, some of them are parallelized.
First, the code is parsed into an abstract syntax tree. Parallel to this, the referenced assemblies are loaded (if not cached) using Mono.Cecil. Then both are converted into a single type-system. This type-system is traversed to find references to the same element and to extract the units from the attributes and xml documentation. After this step, each element has an unit description. An unit description can resolve possible dynamic units into concrete units by analyzing the passed target object and arguments.
In the next step, the whole abstract syntax tree is traversed again (but now parallelized) to validate assignments and method calls by using the extracted units and the inferred units of expressions.
Future
This library is still under development - there are still some bugs and limitations.
Currently, one big issue is the performance: Despite parallelization, it is still quite slow - but there are enough points to optimize.
History