Skip to content

Commit ad445ef

Browse files
angularsenclaude
andauthored
Revert IQuantity.UnitInfo DIM, expose GetUnitInfo() extension on all 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>
1 parent 2a60bf9 commit ad445ef

3 files changed

Lines changed: 41 additions & 38 deletions

File tree

UnitsNet.Tests/CustomCode/IQuantityTests.cs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,44 +60,59 @@ public void ToUnit_UnitSystem_ThrowsArgumentNullExceptionIfNull()
6060
}
6161

6262
[Fact]
63-
public void UnitInfo_ReturnsUnitInfoForQuantityUnit()
63+
public void GetUnitInfo_ReturnsUnitInfoForQuantityUnit()
6464
{
6565
var length = new Length(3.0, LengthUnit.Centimeter);
6666
IQuantity quantity = length;
6767

68-
UnitInfo unitInfo = quantity.UnitInfo;
68+
UnitInfo unitInfo = quantity.GetUnitInfo();
6969

7070
Assert.Equal(nameof(LengthUnit.Centimeter), unitInfo.Name);
7171
Assert.Equal(quantity.UnitKey, unitInfo.UnitKey);
7272
}
7373

7474
[Fact]
75-
public void UnitInfo_Zero_ReturnsBaseUnitInfo()
75+
public void GetUnitInfo_Zero_ReturnsBaseUnitInfo()
7676
{
7777
IQuantity quantity = Length.Info.Zero;
7878

79-
UnitInfo unitInfo = quantity.UnitInfo;
79+
UnitInfo unitInfo = quantity.GetUnitInfo();
8080

8181
Assert.Equal(Length.Info.BaseUnitInfo.UnitKey, unitInfo.UnitKey);
8282
}
8383

8484
[Fact]
85-
public void UnitInfo_TypedQuantity_ReturnsTypedUnitInfo()
85+
public void GetUnitInfo_ConcreteQuantity_ReturnsFullyTypedUnitInfo()
8686
{
87-
IQuantity<LengthUnit> quantity = new Length(3.0, LengthUnit.Centimeter);
87+
var quantity = new Length(3.0, LengthUnit.Centimeter);
8888

89-
UnitInfo<LengthUnit> unitInfo = quantity.UnitInfo;
89+
// Overload resolution picks GetUnitInfo<TQuantity, TUnit> for the concrete struct receiver,
90+
// returning the most specific UnitInfo<Length, LengthUnit>.
91+
UnitInfo<Length, LengthUnit> unitInfo = quantity.GetUnitInfo();
9092

9193
Assert.Equal(LengthUnit.Centimeter, unitInfo.Value);
9294
Assert.Equal(nameof(LengthUnit.Centimeter), unitInfo.Name);
9395
}
9496

9597
[Fact]
96-
public void UnitInfo_MatchesUnit()
98+
public void GetUnitInfo_TypedQuantityReference_FallsBackToNonGeneric()
99+
{
100+
IQuantity<LengthUnit> quantity = new Length(3.0, LengthUnit.Centimeter);
101+
102+
// The IQuantity<TUnit> reference does not satisfy the IQuantity<TSelf, TUnit> constraint
103+
// (TSelf would be IQuantity<MassUnit>), so resolution falls back to GetUnitInfo(IQuantity).
104+
UnitInfo unitInfo = quantity.GetUnitInfo();
105+
106+
Assert.Equal(LengthUnit.Centimeter, ((UnitInfo<LengthUnit>)unitInfo).Value);
107+
Assert.Equal(nameof(LengthUnit.Centimeter), unitInfo.Name);
108+
}
109+
110+
[Fact]
111+
public void GetUnitInfo_MatchesUnit()
97112
{
98113
Assert.All(Quantity.Infos.Select(x => x.Zero), quantity =>
99114
{
100-
Assert.Equal(quantity.Unit, quantity.UnitInfo.Value);
115+
Assert.Equal(quantity.Unit, quantity.GetUnitInfo().Value);
101116
});
102117
}
103118

UnitsNet/Extensions/QuantityExtensions.cs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,41 @@ namespace UnitsNet;
1111
/// </summary>
1212
public static class QuantityExtensions
1313
{
14-
#if !NET
1514
/// <summary>
1615
/// Gets the <see cref="UnitInfo"/> for the unit this quantity was constructed with.
1716
/// </summary>
18-
/// <param name="quantity">The quantity.</param>
19-
/// <returns>The <see cref="UnitInfo"/> for the quantity's unit.</returns>
2017
/// <remarks>
21-
/// On .NET 5+ targets, this is available as a default interface member property
22-
/// <c>IQuantity.UnitInfo</c> instead.
18+
/// Picked by overload resolution for callers that only have an <see cref="IQuantity"/> reference.
19+
/// Concretely-typed callers (e.g. a <c>Mass</c> receiver) bind to the
20+
/// <see cref="GetUnitInfo{TQuantity,TUnit}(IQuantity{TQuantity,TUnit})"/> overload and get the
21+
/// more specific <see cref="UnitInfo{TQuantity,TUnit}"/> return.
2322
/// </remarks>
23+
/// <param name="quantity">The quantity.</param>
24+
/// <returns>The <see cref="UnitInfo"/> for the quantity's unit.</returns>
2425
public static UnitInfo GetUnitInfo(this IQuantity quantity)
2526
{
2627
return quantity.QuantityInfo[quantity.UnitKey];
2728
}
2829

2930
/// <summary>
30-
/// Gets the <see cref="UnitInfo{TUnit}"/> for the unit this quantity was constructed with.
31+
/// Gets the <see cref="UnitInfo{TQuantity,TUnit}"/> for the unit this quantity was constructed with.
3132
/// </summary>
32-
/// <typeparam name="TUnit">The unit enum type.</typeparam>
33-
/// <param name="quantity">The quantity.</param>
34-
/// <returns>The <see cref="UnitInfo{TUnit}"/> for the quantity's unit.</returns>
3533
/// <remarks>
36-
/// On .NET 5+ targets, this is available as a default interface member property
37-
/// <c>IQuantity&lt;TUnitType&gt;.UnitInfo</c> instead.
34+
/// Picked by overload resolution for concretely-typed receivers (e.g. <c>Mass</c>) where C# can
35+
/// infer both <typeparamref name="TQuantity"/> and <typeparamref name="TUnit"/> from the receiver's
36+
/// <see cref="IQuantity{TSelf,TUnit}"/> implementation. Callers with only an <see cref="IQuantity"/>
37+
/// reference fall back to the non-generic <see cref="GetUnitInfo(IQuantity)"/> overload.
3838
/// </remarks>
39-
public static UnitInfo<TUnit> GetUnitInfo<TUnit>(this IQuantity<TUnit> quantity)
39+
/// <typeparam name="TQuantity">The quantity type.</typeparam>
40+
/// <typeparam name="TUnit">The unit enum type.</typeparam>
41+
/// <param name="quantity">The quantity.</param>
42+
/// <returns>The <see cref="UnitInfo{TQuantity,TUnit}"/> for the quantity's unit.</returns>
43+
public static UnitInfo<TQuantity, TUnit> GetUnitInfo<TQuantity, TUnit>(this IQuantity<TQuantity, TUnit> quantity)
44+
where TQuantity : IQuantity<TQuantity, TUnit>
4045
where TUnit : struct, Enum
4146
{
4247
return quantity.QuantityInfo[quantity.Unit];
4348
}
44-
#endif
4549

4650
/// <inheritdoc cref="IQuantity.As(UnitKey)" />
4751
/// <remarks>This should be using UnitConverter.Default.ConvertValue(quantity, toUnit) </remarks>

UnitsNet/IQuantity.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,6 @@ public interface IQuantity : IFormattable
5858
/// as it avoids the boxing that would normally occur when casting the enum to <see cref="Enum" />.
5959
/// </remarks>
6060
UnitKey UnitKey { get; }
61-
62-
#if NET
63-
/// <summary>
64-
/// Gets the <see cref="UnitsNet.UnitInfo"/> for the unit this quantity was constructed with.
65-
/// </summary>
66-
/// <remarks>
67-
/// On targets that do not support default interface members (e.g. netstandard2.0),
68-
/// use the <c>GetUnitInfo()</c> extension method from <see cref="QuantityExtensions"/> instead.
69-
/// </remarks>
70-
UnitInfo UnitInfo => QuantityInfo[UnitKey];
71-
#endif
7261
}
7362

7463
/// <summary>
@@ -105,13 +94,8 @@ public interface IQuantity<TUnitType> : IQuantity
10594

10695
#if NET
10796

108-
/// <inheritdoc cref="IQuantity.UnitInfo"/>
109-
new UnitInfo<TUnitType> UnitInfo => QuantityInfo[Unit];
110-
11197
#region Implementation of IQuantity
11298

113-
UnitInfo IQuantity.UnitInfo => UnitInfo;
114-
11599
QuantityInfo IQuantity.QuantityInfo
116100
{
117101
get => QuantityInfo;

0 commit comments

Comments
 (0)