Skip to content

Simplify interfaces#1662

Open
Muximize wants to merge 3 commits into
angularsen:masterfrom
Muximize:simplify-interfaces
Open

Simplify interfaces#1662
Muximize wants to merge 3 commits into
angularsen:masterfrom
Muximize:simplify-interfaces

Conversation

@Muximize

@Muximize Muximize commented May 4, 2026

Copy link
Copy Markdown
Contributor

In my use of UnitsNet, I find the recently added IQuantityOfType<TQuantity> very useful. I wanted to do a comparison between some, but could not without adding IComparisonOperators<TQuantity, TQuantity, bool> to the constraints of my method. I then realized this can be added to the base interface instead of the quantity itself.

While doing this I found a way to simplify some other interfaces in the inheritance chain, like reusing IArithmeticQuantity for both ILinearQuantity and ILogarithmicQuantity and removing one level of inheritance.

@lipchev What are your thoughts? Is this an improvement?

@angularsen

Copy link
Copy Markdown
Owner

@claude Review idea in PR description and current code change

@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown

Claude finished @angularsen's task in 4m 56s —— View job


PR Review: Simplify interfaces

  • Gather context: read interface files and diff
  • Understand the design idea in PR description
  • Review code changes for correctness and implications
  • Post review feedback

Design Idea

The PR has two goals:

  1. Add IComparisonOperators<TQuantity, TQuantity, bool> to IQuantityOfType<TQuantity> so callers can use comparison operators on generic quantities without extra constraints
  2. Flatten the two-type-parameter interface pattern (IArithmeticQuantity<TSelf, TUnit>, ILogarithmicQuantity<TSelf, TUnit>, IAffineQuantity<TSelf, TUnit, TOffset>) into simpler single-type-param interfaces composed with IQuantity<TSelf, TUnit> directly

Both goals are reasonable. The first is clearly useful. The second reduces redundancy in the generated code significantly.


What Changed

Hierarchy restructure (non-generated files):

Before After
IArithmeticQuantity.cs held both ILinearQuantity<TSelf> and IArithmeticQuantity<TSelf, TUnitType> IArithmeticQuantity.cs holds only IArithmeticQuantity<TSelf>; ILinearQuantity<TSelf> is in its own file
ILogarithmicQuantity<TSelf, TUnitType> existed Removed; only ILogarithmicQuantity<TSelf> : IQuantityOfType<TSelf>, IArithmeticQuantity<TSelf> remains
IAffineQuantity<TSelf, TUnitType, TOffset> existed Removed; only IAffineQuantity<TSelf, TOffset> : IQuantityOfType<TSelf> remains
IQuantityOfType<out TQuantity> — covariant, no comparison interfaces IQuantityOfType<TQuantity> — invariant, adds IComparable<TQuantity>, IEquatable<TQuantity>, IComparisonOperators (NET7+), IParsable (NET7+)
IQuantity : IFormattable IQuantity : IComparable, IFormattable

Generated quantities:

Before (per-quantity flat list):

public readonly partial struct Length :
    IArithmeticQuantity<Length, LengthUnit>,
#if NET7_0_OR_GREATER
    IDivisionOperators<Length, Length, double>,
    IMultiplyOperators<Length, Length, Area>,
    IComparisonOperators<Length, Length, bool>,
    IParsable<Length>,
#endif
    IComparable, IComparable<Length>, IEquatable<Length>, IFormattable

After (clean hierarchy):

public readonly partial struct Length : IQuantity<Length, LengthUnit>, ILinearQuantity<Length>
#if NET7_0_OR_GREATER
    , IMultiplyOperators<Length, Length, Area>
    ...
#endif

Issues

1. Breaking: IDivisionOperators<TSelf, TSelf, double> silently removed

The generator previously emitted IDivisionOperators<Length, Length, double> for all non-affine quantities. The operator itself (public static double operator /(Length left, Length right)) still exists in code, but this interface is no longer declared. External code that constrains on where T : IDivisionOperators<T, T, double> to work with any self-dividing quantity will break silently — the cast still works at runtime, but the constraint won't satisfy.

This removal seems intentional (the old generator emitted it for all non-affine quantities, new generator doesn't), but deserves an explicit mention in the PR description or changelog. Relevant generator diff →

2. Breaking: Removal of covariance from IQuantityOfType

IQuantityOfType<out TQuantity>IQuantityOfType<TQuantity>. Removing the out is required because IComparisonOperators is invariant, but it's still a binary breaking change. Code like IQuantityOfType<IQuantity> where a IQuantityOfType<Length> was expected (covariant assignment) will no longer compile. [IQuantity.cs:122](https://github.com/angularsen/UnitsNet/pull/1662/files#diff-d9f4ac20669a77ba849573acd66b0b5eb73d1bbe)

3. Breaking: IComparable added to base IQuantity

IQuantity now inherits IComparable (IQuantity.cs:11). Custom implementations that don't implement IComparable will break. The HowMuch.cs update shows this impact. For a widely-consumed library, this warrants a changelog entry.

4. Breaking: More interfaces required on IQuantityOfType implementations

On NET7+, IQuantityOfType<TQuantity> now requires IComparisonOperators<TQuantity, TQuantity, bool> and IParsable<TQuantity> from implementors. The HowMuch.cs file shows ~100 lines of new required boilerplate. This is significant for third-party quantity implementors.

5. Structural: Constraint duplication in IQuantityOfType

// IQuantity.cs:122-133
public interface IQuantityOfType<TQuantity> : IQuantity
#if NET7_0_OR_GREATER
    , IComparisonOperators<TQuantity, TQuantity, bool>  // (A) interface inherits this
    , IParsable<TQuantity>
#endif
    , IComparable<TQuantity>
    , IEquatable<TQuantity>
    where TQuantity : IQuantity
#if NET7_0_OR_GREATER
    , IComparisonOperators<TQuantity, TQuantity, bool>  // (B) constraint repeats it
    , IParsable<TQuantity>
#endif

This duplication is technically required for C# to let the compiler use the operators through the type parameter TQuantity, but it's visually confusing. A short comment explaining why both are needed (the interface inheritance satisfies the implementor, while the constraint satisfies callers) would help. IQuantity.cs:119 →

6. Minor: LogarithmicQuantityExtensions constraint is slightly more verbose

where TQuantity : ILogarithmicQuantity<TQuantity, TUnit> was simple. Now it's where TQuantity : ILogarithmicQuantity<TQuantity>, IQuantity<TQuantity, TUnit>. Not a problem, just noting the caller-side impact of the two-param removal.


Positives

  • The generated code is much cleaner — a flat list of 10+ interfaces per quantity becomes just 2-3
  • ILinearQuantity and ILogarithmicQuantity now share IArithmeticQuantity<TSelf> consistently, making the hierarchy semantically correct
  • IComparisonOperators in IQuantityOfType is genuinely useful for generic code
  • IParsable in IQuantityOfType on NET7+ is a nice-to-have
  • IAffineQuantity<TSelf, TOffset> now correctly includes the arithmetic operators it was missing (they were only on the removed three-param version)

Recommendation

The direction is good. The main concern is that this is a significant breaking change for custom quantity implementors and for any external code constraining on the old two-param interfaces or on IDivisionOperators<T, T, double>. If the library is on a major version or the team is comfortable with the break, this is a clear improvement. Otherwise, it warrants a migration guide or at least a CHANGELOG entry listing the removed interfaces.

The HowMuch.cs updated example is helpful for showing the new implementation contract. It might also be worth updating Docs/ to reflect the new interface requirements for custom quantities.

@Muximize
Muximize force-pushed the simplify-interfaces branch from 4a7a992 to bd39d28 Compare June 11, 2026 22:13
@Muximize

Copy link
Copy Markdown
Contributor Author

@angularsen If it helps for reviewing I can split up this PR, keeping the simplifying here and make a separate PR for adding IComparisonOperators.

@lipchev

lipchev commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

As I was refactoring the interfaces last time, I actually did pretty much the same thing - but in the end decided to revert it due to the following consideration: the INumber interface (even our partial implementation) is quite large. While I personally would probably always go all-the-way in implementing all possible interfaces in my custom quantities, this maybe more of a headache for others - who just care about the unit conversion / type safety.

@Muximize
Muximize force-pushed the simplify-interfaces branch from bd39d28 to 5df56f4 Compare July 17, 2026 12:39
@Muximize

Muximize commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

I removed the IComparisonOperators stuff from this PR, so it is now purely about simplifying ILinearQuantity, ILogarithmicQuantity and IAffineQuantity, removing 1 layer of inheritance.

@Muximize

Copy link
Copy Markdown
Contributor Author

@lipchev Yes indeed, moving interfaces up will make things harder for custom quantities. I was struggling a bit with HowMuch, but I don't use them myself. What is you usecase for custom quantities?

@angularsen

Copy link
Copy Markdown
Owner

@claude review

@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown

Claude finished @angularsen's task in 3m 47s —— View job


PR Review: Simplify interfaces (updated)

  • Gather context: read latest diff and prior review discussion
  • Understand current state of PR (scope narrowed — IComparisonOperators/IParsable/IComparable additions were dropped per the thread below)
  • Review code changes for correctness and implications
  • Post updated review feedback

Scope has changed since the last review

Per the discussion with @lipchev, the IComparisonOperators/IParsable/IComparable additions to IQuantityOfType were removed from this PR. I re-read the current state of the branch and confirmed:

  • IQuantity still only inherits IFormattable (UnitsNet/IQuantity.cs:9) — no IComparable added.
  • IQuantityOfType<out TQuantity> is still covariant (UnitsNet/IQuantity.cs:132) — no breaking loss of covariance.
  • No IComparisonOperators/IParsable constraints added to IQuantityOfType.
  • HowMuch.cs (the custom/third-party quantity example) has no diff at all against master — third-party implementors of the base IQuantity<TSelf, TUnit> are unaffected.

So all four of my previously-raised breaking-change concerns (items 1–4 in the earlier review) no longer apply to the current diff. This is now a much narrower, lower-risk change purely about the interface hierarchy for ILinearQuantity/ILogarithmicQuantity/IAffineQuantity.

What's left in scope

  • IArithmeticQuantity<TSelf> (UnitsNet/IArithmeticQuantity.cs:15) is now a small, standalone interface with just the operator constraints (IAdditionOperators, ISubtractionOperators, IMultiplyOperators<TSelf,double,TSelf>, IDivisionOperators<TSelf,double,TSelf>, IUnaryNegationOperators).
  • ILinearQuantity<TSelf> (UnitsNet/ILinearQuantity.cs:29) and ILogarithmicQuantity<TSelf> (UnitsNet/ILogarithmicQuantity.cs:21) both now compose IQuantityOfType<TSelf> + IArithmeticQuantity<TSelf>, which is the stated goal — one shared mixin instead of duplicated operator lists.
  • IAffineQuantity<TSelf, TOffset> (UnitsNet/IAffineQuantity.cs:15) picked up IAdditionOperators<TSelf, TOffset, TSelf> / ISubtractionOperators<TSelf, TSelf, TOffset>, which previously only existed on the now-removed three-param IAffineQuantity<TSelf, TUnitType, TOffset> — a real (small) bug fix, since the two-param interface was missing these before.
  • Generated quantities now implement IQuantity<TSelf, TUnit>, ILinearQuantity<TSelf> (or ILogarithmicQuantity<TSelf> / IAffineQuantity<TSelf, TOffset>) directly instead of the old two/three-param interfaces (see Length.g.cs:36-37 vs. the generator diff in CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs:103-126).
  • I grepped the whole codebase for leftover references to the removed IArithmeticQuantity<TSelf, TUnitType>, ILogarithmicQuantity<TSelf, TUnitType>, and IAffineQuantity<TSelf, TUnitType, TOffset> — found none; LogarithmicQuantityExtensions.cs was correctly updated to the new constraint shape (where TQuantity : ILogarithmicQuantity<TQuantity>, IQuantity<TQuantity, TUnit>).

Remaining considerations

  1. Still a public breaking change, just smaller. IArithmeticQuantity<TSelf, TUnitType>, ILogarithmicQuantity<TSelf, TUnitType>, and IAffineQuantity<TSelf, TUnitType, TOffset> are removed entirely. Any external code with a constraint like where T : IArithmeticQuantity<T, TUnit>, or a field/variable typed as one of these, will fail to compile. Given the package is still pre-1.0 (6.0.0-pre*), this is likely acceptable, but a short CHANGELOG note listing the removed interface names would help consumers doing the upgrade.
  2. LogarithmicQuantityExtensions callers get a slightly more verbose constraint (ILogarithmicQuantity<TQuantity>, IQuantity<TQuantity, TUnit> instead of a single ILogarithmicQuantity<TQuantity, TUnit>) — minor, just worth noting as caller-visible.
  3. No CHANGELOG/Docs/ update is included in the current diff. Since this touches part of the public interface surface documented for custom quantities, it may be worth a small mention in Docs/adding-a-new-unit.md or a migration note, even though the concrete impact is now much smaller.

Verification

  • PR Build CI is green (latest run: success).
  • I wasn't able to run a local dotnet build/generate-code in this sandboxed environment (network/build commands required approval that wasn't available here), so I relied on static diff/grep verification above rather than a fresh compile.

Overall

The direction is good and the risk profile dropped significantly once the IComparisonOperators piece was split out — this version is a clean, low-risk simplification of the generated interface hierarchy with one small correctness improvement (IAffineQuantity operators). Main ask: call out the interface removals in the changelog since it's still a public breaking change, even if minor pre-1.0.
·

/// </remarks>
public static TQuantity Sum<TSource, TQuantity, TUnit>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, TUnit targetUnit)
where TQuantity : ILogarithmicQuantity<TQuantity, TUnit>
where TQuantity : ILogarithmicQuantity<TQuantity>, IQuantity<TQuantity, TUnit>

@angularsen angularsen Jul 18, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest keeping the two-parameter quantity interfaces, if viable.

I think this is simpler for consumers: they can choose between the unit-agnostic and unit-specific interfaces, instead of repeating both constraints in their own APIs:

where TQuantity : ILogarithmicQuantity<TQuantity>, IQuantity<TQuantity, TUnit>

A composite interface captures that relationship directly:

public interface ILogarithmicQuantity<TSelf, TUnit> :
    IQuantity<TSelf, TUnit>,
    ILogarithmicQuantity<TSelf>
    where TSelf : ILogarithmicQuantity<TSelf, TUnit>
    where TUnit : struct, Enum
{
}

This matches the existing shape of the quantity interfaces (before the PR):

IArithmeticQuantity<TSelf, TUnit>
IAffineQuantity<TSelf, TUnit, TOffset>
ILogarithmicQuantity<TSelf, TUnit>

To me, this represents a useful concept: “a logarithmic quantity with a known unit enum”, while avoiding repeated multi-constraint combinations in public APIs.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this goes for all the interfaces; affine, logarithmic, arithmetic.

Can we preserve the two-parameter composite interfaces to avoid consumers having to specify multiple generic constraints?

@angularsen angularsen Jul 18, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed fix that I think works, take a look if you agree. I tried to minimize diff in PR also, we can refactor later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants