In our large code base (measurement processing application) we've had bugs where values were mixed up because everything is a double. So we decided to try-out strongly named types for things like Potential, PotentialRate, Duration, etc resulting in ~30 types
e.g.
public readonly record struct Potential
{
private AppliedPotential(double valueInVolt)
{
_internalValue = valueInVolt;
}
public static AppliedPotential FromVolt(double valueInVolt) => new(valueInVolt);
public double InVolt => _internalValue;
//[...more implementation like operator overloads...]
private readonly double _internalValue;
}
Since we want to do calculations with these we want to have operator overloads. But for all these types these have the same implementation:
public static [Type] operator +([Type] left, [Type] right) => new(left._internalValue + right._internalValue);
public static [Type] operator -([Type] left, [Type] right) => new(left._internalValue - right._internalValue);
public static [Type] operator *([Type] value, double factor) => new(value._internalValue * factor);
public static double operator /([Type] left, [Type] right) => left._internalValue / right._internalValue;
public static bool operator >([Type] left, [Type] right) => left._internalValue > right._internalValue;
public static bool operator >=([Type] left, [Type] right) => left._internalValue >= right._internalValue;
public static bool operator <([Type] left, [Type] right) => left._internalValue < right._internalValue;
public static bool operator <=([Type] left, [Type] right) => left._internalValue <= right._internalValue;
public int CompareTo([Type] other) => _internalValue.CompareTo(other._internalValue);
// etc etc
Over 30 types that a lot of duplicate code and not very DRY!
Question
Is there a way to combine all the duplicate code, so there's less boilerplate code to maintain?
N.b. I can't use source generators, since we found they are hard to maintain and decided we aren't making them ourselves anymore.
Own attempts
I've been looking into having some common implementation. In C++ I would use templates, but C# doesn't have those. And generic classes are something different. But a bigger issue is that the struct doesn't even allow inheritance! (again: C++ does allow that... I switched from C++ to C# 5 years ago, but I often I miss it...) I've looked at "the next best thing": default implementations on interfaces. So I made an IStrongDouble<TSelf> interface like this:
public interface IStrongDouble<TSelf>
where TSelf : IStrongDouble<TSelf>
{
double Value { get; }
static abstract TSelf Create(double value);
public static TSelf operator +(TSelf left, TSelf right) =>
TSelf.Create(left.Value + right.Value);
}
But that doesn't compile, giving a CS0563 error:
One of the parameters of a binary operator must be the containing type
I've been trying to get around it, but haven't succeeded...
e.g. I've tried implementing the operator overload like
public static TSelf operator +(IStrongDoubleNumber<TSelf> left, IStrongDoubleNumber<TSelf> right) =>
TSelf.Create(left.Value + right.Value);
Which doesn't give the error... however the operator overload is not detected when adding the values. E.g.
var result = Potential.FromVolt(1) + Potential.FromVolt(2);
would give "operator '+' cannot be applied to operands of type 'Potential' and 'Potential'".
I'd have to write it like: var result = (IStrongDoubleNumber<Potential>)Potential.FromVolt(1) + Potential.FromVolt(2);, which is awful!
(Now don't close this question as "dupe" again for this attempt: this attempt is not my question, my question is above)
I've tried to circumvent this by implementing an implicit cast operator
public static implicit operator IStrongDoubleNumber<[Type]>([Type] source) => source;.
But that gives a new error "user-defined conversions to or from an interface are not allowed"...
(Why is C# making my life so hard!)
Reaction to earlier feedback
In the comments below my previous question Sweeper suggested
Wouldn't it be enough to just declare one such type, with a generic parameter, and then declare some "dummy" types for each kind of value? e.g.
Measurement<Potential>,Measurement<Current>, and so on. –
That would not work:
- firstly, the types have specific way they're created and read: e.g. create
var potential Potential.FromVolt(value)and readdouble potentialInVolt = potential.InVolt;. These differ per type. - secondly, some types have specific operator overloads to convert between types (and operator overloads required to be defined in the type definition). E.g.
public static Duration operator /(Current left, CurrentRate right) =>
Duration.FromSeconds(left._internalValue / right._internalValue);
(For simplicity I'm leaving out how both internal values are accessible here. Assume it works)
Measurement<Potential>,Measurement<Current>, and so on.+operator "through" an interface.IAdditionOperators<TSelf, TSelf, TSelf>? I was expecting that the compiler would be changed to internally cast it to that type (or whatever the compiler/interpreter does internally)... So if it doesn't, I think it's an oversight that they didn't use that...IStrongDoubleNumber) more easily. But in this case you are writing code that specifically works withMyType, which is not the main point of the feature.