1

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 read double 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)

9
  • 2
    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. Commented Jun 15 at 8:41
  • @Sweeper that might be a good idea. I'll look into it. however, this question is more about the C# language and it's capabilities. I'll update the post. Commented Jun 15 at 8:53
  • 1
    Well if that's the case then this is a duplicate of stackoverflow.com/q/58950859/5133585 TLDR: no. You can only use the + operator "through" an interface. Commented Jun 15 at 8:55
  • @Sweeper in that case: what's the added value of the static numerics interfaces (like 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... Commented Jun 15 at 9:05
  • 2
    The value is that you can write generic code, (code that works with anything implementing IStrongDoubleNumber) more easily. But in this case you are writing code that specifically works with MyType, which is not the main point of the feature. Commented Jun 15 at 9:06

2 Answers 2

1

As of .NET 9 / C# 13 [1], I don't think it is possible to do exactly what you want without using source generators. I was able to resolve your CS0563 error by modifying your types and introducing generic math as follows: [2]

public interface IStrongDouble<TSelf> : IAdditionOperators<TSelf, TSelf, TSelf> 
    where TSelf : IStrongDouble<TSelf>
{
    double Value { get; }

    static abstract TSelf Create(double value);
    static TSelf IAdditionOperators<TSelf, TSelf, TSelf>.operator +(TSelf left, TSelf right) => TSelf.Create(left.Value + right.Value);
}

public readonly record struct Potential : IStrongDouble<Potential>
{
    private readonly double _internalValue;
    private Potential(double valueInVolt) => this._internalValue = valueInVolt;

    public static Potential Create(double valueInVolt) =>  new(valueInVolt);
    public double Value => _internalValue;
}

Having done that, I could now write generically constrained code for Potential as follows, and it would compile and run successfully even though the addition operator was only implemented by IStrongDouble<TSelf>:

static TSelf TestAdd<TSelf>(TSelf number, double addend) where TSelf : IStrongDouble<TSelf> =>
    number + TSelf.Create(addend);

var p = TestAdd(Potential.Create(1), 2);

However, if I attempt to use the addition operator for Potential directly rather than in a generically constrained context, e.g.:

var p = Potential.Create(1) + Potential.Create(2);

It would not compile, and generated an error:

Operator '+' cannot be applied to operands of type 'Potential' and 'Potential'

This seems to be because operators in base interfaces will not be detected as overload candidate as per C# Draft Specification §12.4.6: Candidate user-defined operators:

  • Otherwise, the set of candidate operators provided by T₀ is the set of candidate operators provided by the direct base class of T₀, or the effective base class of T₀ if T₀ is a type parameter.

Unfortunately, interfaces are never "direct base types" of classes or structs in .NET, they are only ever "effective base types" of constrained type parameters.

Demo fiddle here.

Finally, it seems that "extension operators" aren't implemented either, see Extension operators #515 (unanswered), Champion "Extension function members" #192 (open) and [Proposal]: extensions #8697 (open). So there's no workaround available with extension methods either.


[1] Update for .NET 10 preview 5

In the future, Extension operators in .NET 10 / C# 14 may be what you need. While still in preview at the time of this answer, see the following documents:


[2] Here I had to implement the addition operator + explicitly in IStrongDouble<TSelf> because of the following requirement in the design document default interface methods: Explicit implementation in interfaces:

Explicit implementations allow the programmer to provide a most specific implementation of a virtual member in an interface where the compiler or runtime would not otherwise find one. An implementation declaration is permitted to explicitly implement a particular base interface method by qualifying the declaration with the interface name (no access modifier is permitted in this case). Implicit implementations are not permitted.
Sign up to request clarification or add additional context in comments.

1 Comment

Have a look at my answer based on C# 11
-2
static abstract TSelf operator +(TSelf left, TSelf right);

Yes, this compiles. Yes, it works. Yes, it’s the correct approach.

If this looks wrong to you, update your compiler—or your expectations.

This is part of C# 11’s support for static abstract members in interfaces, which finally lets us write generic math code without duplicating operator overloads across every numeric type. It’s not a hack. It’s not a workaround. It’s the language evolving.

If you’re still confused, I recommend doing what the rest of us did: read the release notes and try it in a project.


Yes, each struct has to implement the operator. That’s how `static abstract` works, this is by design. It’s not a flaw — it’s the entire point, it enforces consistency, enables generic logic, and avoids runtime garbage like reflection, boxing or dynamic dispatch unlike other "DRY" hacks that sacrifice performance. If you're expecting zero boilerplate across 30+ types without source generators, inheritance, or macros, you're asking for something C# doesn't offer. This is as DRY as it gets without breaking the type system. You write one line per type. In return, you get reusable, type-safe, high-performance generic math. That’s a trade any serious developer should take. So yes, each struct writes one line of operator code. But the generic math logic? That’s now centralized, reusable, and future-proof. If that’s still too much effort, maybe the problem isn’t the language. This is the language doing exactly what it was designed to do, if that's not good enough should take it up with the C# development team, preferably after reading the specs. It appears the OP wants magic tricks not maintainable code.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.