Software Engineering
c# object-oriented measurement
Updated Fri, 09 Sep 2022 00:24:45 GMT

How to come up with an easy-to-use way of creating quantities with units in C#?


I want to come up with a way to make it easy to write classes that represent quantities with units, such as length, weight, etc. For example,

var height = new Length(32.2, LengthUnit.M);
var weight = new Weight(164.2, WeightUnit.Lb);

In addition, these classes should automatically handle unit conversions and implement common mathematical operations. For example,

var length1 = new Length(3.36, LengthUnit.M);
var length2 = new Length(34.5, LengthUnit.Cm);
var totalLength = length1 + length2;

My initial solution was to create a base class Quantity<TUnit>, where TUnit is a "unit" type (for example, LengthUnit) that defines scale conversions. But I'm running into the issue that my operator overloading methods can't possibly know the derived type to return. This means that in the example above, totalLength is of type Quantity<LengthUnit>, not Length. If C# had a way to create an alias for a class, so that Length was equivalent to Quantity<LengthUnit>, things would work, but C# lacks such a feature!

I could pass on the work of defining all the operator overloading methods to the derived classes, but this wouldn't make it an "easy" way to create new quantity types.

Any better ideas?

Update: Here's an implementation of my initial solution.

public abstract class Unit
{
    protected Unit(string symbol, double factor)
    {
        Symbol = symbol;
        Factor = factor;
    }
    public string Symbol { get; }
    public double Factor { get; }
}
public class Quantity<TUnit> where TUnit : Unit
{
    private readonly double baseValue;
    protected Quantity(double value, TUnit unit)
    {
        baseValue = value * unit.Factor;
    }
    private Quantity(double value)
    {
        baseValue = value;
    }
    public static Quantity<TUnit> operator +(Quantity<TUnit> a, Quantity<TUnit> b)
    {
        return new Quantity<TUnit>(a.baseValue + b.baseValue);
    }
}
public class LengthUnit : Unit
{
    public static readonly LengthUnit M = new LengthUnit("m", 1);
    public static readonly LengthUnit Cm = new LengthUnit("cm", 0.01);
    protected LengthUnit(string symbol, double factor)
        : base(symbol, factor) { }
}
public class Length : Quantity<LengthUnit>
{
    public Length(double value, LengthUnit unit)
        : base(value, unit) { }
}



Solution

To try to answer the question directly, here is my not-so-pretty implementation

For base class, we need to force TSelf into type parameter, so we can make a generic operator overloading on it.

public abstract class Quantity<TSelf, TUnit> 
    where TUnit : Unit
    where TSelf : Quantity<TSelf, TUnit>
{
    protected readonly double baseValue;
    protected Quantity(double value, TUnit unit)
    {
        baseValue = value * unit.Factor;
    }
    private Quantity(double value)
    {
        baseValue = value;
    }
    public abstract TSelf WithValue(double value);        
    public static TSelf operator +(TSelf a, Quantity<TSelf, TUnit> b)
    {
        return a.WithValue(a.baseValue + b.baseValue);
    }
}

Notice the WithValue method. This exists so we can create a quantity in the same unit but with different value.

For the Length class, we need to implement WithValue manually, yet it is trivial:

public class Length : Quantity<Length, LengthUnit>
{    
    public Length(double value, LengthUnit unit)
        : base(value, unit) { }
    public override Length WithValue(double value) => new Length(value, LengthUnit.M);
}

While the declaration is not so intuitive, e.g. it is strange to declare Length as Quantity<Length, LengthUnit> and WithValue method seems out of places, I think it solves the OP question to make it easy to create custom quantity type.





Comments (4)

  • +0 – Great solution! I had given up and started using T4 to generate the code for each quantity type, but this is better (even if a bit awkward). Thanks. — Jul 20, 2022 at 14:45  
  • +0 – In Quantity, is there any difference between TSelf and Quantity<TSelf, TUnit>, i.e. couldn't it be TSelf operator+(TSelf, TSelf)? — Jul 21, 2022 at 08:24  
  • +0 – @Caleth It could be TSelf, TSelf. The minimum requirement for C# operator overloading is that at least one of them must be TSelf, so I put it that way. — Jul 21, 2022 at 09:39  
  • +0 – @Caleth I was wrong. It couldn't be TSelf, TSelf because at least one of them must be Quantity<TSelf, TUnit> — Jul 21, 2022 at 16:24