Add static abstract Info and obsolete instance QuantityInfo on .NET 5+#1658
Add static abstract Info and obsolete instance QuantityInfo on .NET 5+#1658angularsen wants to merge 1 commit intomasterfrom
Conversation
|
I'm not saying this is the "right path forward" (I honestly don't know) - but back when I was "rationalizing the existence" of the This is similar in spirit to the Obviously, such a property couldn't go into the standard json format (the two sides would need to use compatible quantity-configurations). |
|
These changes are straight from the bot, but I just wanted to get your eyes on the direction. I am also pondering the removal of the On one hand, it feels more correct to enforce a single QuantityInfo instance per quantity type. I don't see a good use case for two Mass values having different QuantityInfo instances. It's just simpler. On the other hand, instance members work with netstandard and a reference pointer is cheap enough. What are the benefits you see of keeping it an instance member? |
|
Code ReviewOverviewAdds static abstract Breaking Changes
DesignGood overall. A few observations:
TestsFour new tests in SummarySolid improvement that eliminates per-instance boxing and virtual dispatch on |
…TFMs (#1657) ## Summary Reverts the default interface members added in #1649 and exposes `GetUnitInfo()` as extension methods on all target frameworks instead. The typed overload returns the most specific `UnitInfo<TQuantity, TUnit>` when overload resolution can infer both type parameters from the receiver. ## Rationale Per @lipchev's [review feedback on #1649](#1649 (comment)): - **Lean interface.** The DIM is just a getter over `QuantityInfo[UnitKey]` — no new state, sets a precedent for further interface bloat (`Dimensions` etc.). - **Semantic trap.** A custom `IQuantity` implementor overriding `UnitInfo` would silently diverge from internal lookups (`UnitConverter`, `UnitAbbreviationsCache`, `QuantityFormatter`) which all go through `QuantityInfo`, not the new property. - **Boxing on structs.** Calling `Mass.Zero.UnitInfo` through `IQuantity` on a struct quantity boxes the receiver per call; concrete quantities don't override the property so the DIM path is the only one available. The extension method gives the same call-site ergonomics (`quantity.GetUnitInfo()`) on every TFM with none of these issues. ## Overload resolution | Caller has | Resolved overload | Return type | |--------------------|-------------------|-------------| | `Length q;` | `GetUnitInfo<TQuantity, TUnit>(IQuantity<TQuantity, TUnit>)` | `UnitInfo<Length, LengthUnit>` | | `IQuantity q;` | `GetUnitInfo(IQuantity)` | `UnitInfo` | | `IQuantity<TUnit> q;` | `GetUnitInfo(IQuantity)` (fallback) | `UnitInfo` | The `IQuantity<TUnit>` reference case loses its typed return — `IQuantity<TUnit>` does not satisfy the `IQuantity<TQuantity, TUnit>` constraint, so resolution falls back to the non-generic overload. That scenario is uncommon compared to the concretely-typed receiver case, which is now strictly better than before (returns `UnitInfo<TQuantity, TUnit>` instead of `UnitInfo<TUnit>`). ## Follow-up PR #1658 stacks on top of this branch: adds `static abstract Info` on `IQuantityOfType<T>` / `IQuantity<TSelf, TUnitType>`, obsoletes the instance `QuantityInfo` member, and provides matching `GetQuantityInfo()` extensions. Out of scope for this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per discussion on #1657, this introduces a static abstract `Info` member on the IQuantityOfType<TQuantity> and IQuantity<TSelf,TUnitType> interfaces under #if NET, marks the existing instance QuantityInfo property as [Obsolete] on .NET 5+, and adds GetQuantityInfo() extension methods on QuantityExtensions so callers have a single discoverable API that works on every TFM. Why - The instance QuantityInfo property invariably returns a per-type static value. Exposing it as an instance member implies it can vary per instance, which it cannot, and incurs interface dispatch (boxing on structs) for every call. - The static abstract member lets generic algorithms reach the info with `TSelf.Info` directly, no boxing, no virtual call. - The extension method pair (`GetQuantityInfo()` / `GetQuantityInfo<TUnit>()`) is the discoverable replacement for callers that only have an `IQuantity` reference. It looks the quantity up via `UnitsNetSetup.Default.Quantities`. - Keeping the instance property obsolete (warning) instead of removing it preserves source compatibility for existing callers and the netstandard2.0 contract. We can promote to error / remove once netstandard2.0 is dropped. Implementation notes - Generated quantities already expose `public static QuantityInfo<TSelf, TUnitType> Info { get; }`, which directly satisfies the typed static abstract. The non-generic `IQuantityOfType<TSelf>.Info` is satisfied by a default static implementation in IQuantity<TSelf,TUnitType>: `static QuantityInfo IQuantityOfType<TSelf>.Info => TSelf.Info;`. No codegen change required. - The IQuantity bridge `QuantityInfo IQuantity.QuantityInfo => QuantityInfo;` chain inside the interfaces uses #pragma to suppress the obsolete warning on the bridge itself. - Internal callers in UnitsNet were migrated to either `TSelf.Info` / `TSelf.From` (where the generic constraint allows) or `quantity.GetQuantityInfo()` (where it doesn't). Callers that must keep working for custom quantities not registered in `UnitsNetSetup.Default` (JsonNet serialization, debugger proxy, QuantityTypeConverter) keep using the instance member with a `#pragma warning disable CS0618` and a comment explaining why. - HowMuch test custom quantity changed `public static readonly` field to `public static QuantityInfo Info { get; }` property to satisfy the static abstract. - Tests added for round-trip equivalence between `Mass.Info`, `mass.GetQuantityInfo()`, `TQuantity.Info` (via static abstract on IQuantityOfType<T>), and `TSelf.Info` (via static abstract on IQuantity<TSelf,TUnit>). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e3aee59 to
1067c30
Compare
PR Review: Add static abstract Info and obsolete instance QuantityInfo on .NET 5+Overview: Adds Note: The PR description says it's based on #1657 — that must merge and this PR rebase before landing. Positive
ConcernsPotential breaking change in // Before:
return quantity.QuantityInfo[quantity.UnitKey];
// After:
return quantity.GetQuantityInfo()[quantity.UnitKey];
Similarly,
return (QuantityInfo<TUnit>)UnitsNetSetup.Default.Quantities.GetQuantityInfo(quantity.GetType());The cast will throw Missing test: Minor
Overall: The design is sound and the implementation is clean. The main concern is the subtle behavioral breaking change in the |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1658 +/- ##
======================================
- Coverage 96% 96% -1%
======================================
Files 450 450
Lines 29150 29154 +4
======================================
+ Hits 28109 28112 +3
- Misses 1041 1042 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
Builds on top of #1657 (revert of the
IQuantity.UnitInfoDIM) with the bigger ergonomics + perf shift @lipchev called for in his review. Base this PR on #1657 and rebase once it merges.Infomember onIQuantityOfType<TQuantity>andIQuantity<TSelf, TUnitType>(under#if NET).QuantityInfoproperty as[Obsolete]on .NET 5+ across all three interfaces (IQuantity,IQuantity<TUnitType>,IQuantity<TSelf, TUnitType>). Kept un-obsoleted on netstandard2.0 to preserve back-compat there.GetQuantityInfo()extension methods (non-generic + typed) onQuantityExtensionsso callers have a single discoverable API on every TFM.TSelf.Info(where the generic constraint allows) orquantity.GetQuantityInfo()(where it doesn't).Why
The instance
QuantityInfoproperty invariably returns a per-type static value. Exposing it as an instance member:IQuantityimplementors to override it in ways that silently diverge from the rest of the library, which only ever consultsQuantityInfoindirectly through the staticTSelf.Info(via UnitsNetSetup, etc.).The static abstract member lets generic algorithms reach the info with
TSelf.Infodirectly — no boxing, no virtual call. The extension method covers the commonIQuantity/IQuantity<TUnit>reference case via aUnitsNetSetup.Default.Quantitieslookup.Implementation notes
public static QuantityInfo<TSelf, TUnitType> Info { get; }. This directly satisfies the typed static abstract — no codegen change required.IQuantityOfType<TSelf>.Infois satisfied by a default static implementation in the typed interface:QuantityInfo IQuantity.QuantityInfo => QuantityInfo;inside the interface itself are wrapped in#pragma warning disable CS0618so the bridge implementation does not warn about consuming the obsolete member it bridges.UnitsNetSetup.Default(JsonNet serialization, the debugger proxy,QuantityTypeConverter) keep using the instance member with a#pragma warning disable CS0618and a comment explaining why. The lookup-based extension method would throwQuantityNotFoundExceptionfor these scenarios.HowMuchtest custom quantity hadpublic static readonly QuantityInfo Info = ...(a field). Static abstract property requires a property — changed topublic static QuantityInfo Info { get; }.Migration path
IQuantityq.GetQuantityInfo()q.GetQuantityInfo()(delegates to instance)IQuantity<TUnit>q.GetQuantityInfo()(typed return)q.GetQuantityInfo()TQuantity : IQuantityOfType<T>TQuantity.Info(no boxing)q.GetQuantityInfo()TSelf : IQuantity<TSelf, TUnit>TSelf.Info(typed, no boxing)q.QuantityInfoThe instance member stays warning-only this release. Promote to error / remove once netstandard2.0 is dropped — possibly backport the obsolete to v5 if we decide to fully remove in v6.
Tests
GetQuantityInfo_NonGeneric_ReturnsRegisteredQuantityInfo—Same(Mass.Info, ((IQuantity)mass).GetQuantityInfo())GetQuantityInfo_Typed_ReturnsRegisteredQuantityInfo— typed variant returnsQuantityInfo<MassUnit>StaticAbstract_Info_ReturnsSameAsTypedInfo—TSelf.InfoviaIQuantity<TSelf, TUnit>static abstractStaticAbstract_Info_NonGeneric_ReturnsSameAsTypedInfo—TQuantity.InfoviaIQuantityOfType<TQuantity>static abstract (covariant return through default impl bridge)Test plan
dotnet build UnitsNet.slnx— 0 errors across all TFMsdotnet test UnitsNet.Tests -f net8.0— 42303 passed (4 new), 0 faileddotnet test UnitsNet.Serialization.JsonNet.Tests -f net8.0— 113 passed, 0 failedIQuantity.QuantityInfoCS0618 warnings remain inUnitsNet/,UnitsNet.Serialization.JsonNet/,UnitsNet.Benchmark/,Samples/(suppressions are explicit + commented)Out of scope
.QuantityInfocall sites (suppressed at csproj level with<NoWarn>CS0618</NoWarn>).GetQuantityInfo<TSelf, TUnit>(this TSelf q) where TSelf : IQuantity<TSelf, TUnit>overload that would JIT-specialize per struct. Easy to add later if a perf-sensitive use case shows up.🤖 Generated with Claude Code