-
Notifications
You must be signed in to change notification settings - Fork 386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GetHashCode returns different hash for Equal units #1017
Comments
I managed to "fix" it, at least for my needs, with the following extension: public static int GetUnitsNetHashCode<TUnit>(this TUnit unit)
{
if (unit is IQuantity quantity)
{
var valueOfUnitAsBaseUnitRounded = Math.Round(quantity.As(quantity.QuantityInfo.BaseUnitInfo.Value), ComparingConstants.DecimalPlaces, MidpointRounding.AwayFromZero);
return (new
{
quantity.QuantityInfo.Name,
valueOfUnitAsBaseUnitRounded,
quantity.QuantityInfo.BaseUnitInfo.Value
}).GetHashCode();
}
else
{
return unit?.GetHashCode() ?? 0;
}
} The following test now succeeds: [TestMethod]
public void MyTestMethod()
{
Length inCm = Length.FromCentimeters(100);
Length inM = Length.FromMeters(1);
Assert.IsTrue(inCm.Equals(inM));
Assert.AreNotEqual(inCm.GetHashCode(), inM.GetHashCode());
Assert.AreEqual(inCm.GetUnitsNetHashCode(), inM.GetUnitsNetHashCode());
} So instead of using the regular /// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
int hashCode = base.GetHashCode();
hashCode = (hashCode * 397) ^ (Member2D != null ? Member2D.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ Edge;
hashCode = (hashCode * 397) ^ Index;
hashCode = (hashCode * 397) ^ Section.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ Mx.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ My.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ Mxy.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ Vx.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ Vy.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ Nx.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ Ny.GetUnitsNetHashCode();
hashCode = (hashCode * 397) ^ Nxy.GetUnitsNetHashCode();
return hashCode;
}
} |
Another case where it goes wrong: Dictionary<Length, string> test = new Dictionary<Length, string>();
Length one = Length.FromMeters(1);
Length two = Length.FromCentimeters(100);
test.Add(one, "The one with meters");
test.Add(two, "The one with centimeters");
Assert.AreEqual(2, test.Count);
Assert.AreEqual("The one with meters", test[one]);
Assert.AreEqual("The one with centimeters", test[two]);
Assert.AreNotEqual(one.GetHashCode(), two.GetHashCode());
Assert.AreEqual(one, two); There should only be 1 item in the dictionary? |
Luckily, my fix doesn't seem to break stuff (found this test in #541) var length = new Length(1.0, (LengthUnit)2);
var area = new Area(1.0, (AreaUnit)2);
Assert.AreNotEqual(length, area);
Assert.AreNotEqual(length.GetHashCode(), area.GetHashCode());
Assert.AreNotEqual(length.GetUnitsNetHashCode(), area.GetUnitsNetHashCode()); |
@angularsen or @tmilnthorp any thoughts? |
Hi, will try to respond soon, need to digest it all first |
This is a big can of worms. I probably can't summarize it very well, it's been a long time, but I believe it goes something like this: Current state
This often bites people in the ass due to subtle rounding errors. After much back and forth we just haven't been able to agree on a way to implement it that feels correct and intuitive. 3 solutionsTwo of these are discussed in #717 and #838, the PR that got closest to addressing this before it lost its momentum. 1 Strict equalityLet Equals/GetHashCode be based on If the value + unit doesn't match exactly, they are not equal. Unintuitively, these are also not equal: 2 Rounded equalityPerform some rounding to mitigate most rounding errors in practice. However, let's say we agree on rounding to integers just to make a point.
Now, let's agree to round to 1e-5 as was proposed as a sane default. #838 tried to find a universal rounding function and it kind of seemed to work with several test cases around it, but it all got very complicated to understand and be sure that would work well for all sizes of numbers and units, including double vs decimal. In the end the author also agreed that this may not be the way to go. 3 Cop out and remove IEquatableThis is currently where we are headed in #982 unless someone can champion a better solution. We offer overloads of Closing wordsIt's a big wall of text, but since you are currently facing this, what do you think would be the better solution here? cc @lipchev for comments |
I think the approach that @dschuermans is using in the I don't know if there is a reasonable default rounding precision that can be assumed when comparing using the default Equals/GetHashCode methods. By the way- I just recently discovered how Delphi 6 implements the
However in #838 I didn't actually go with the rounding approach- but was rather relying (at least initially) on the use of the IComparable interface - which is still not fixed (this test would fail in the current code-base). This main problem with the IComparable approach was that it isn't transitive - X.ToUnit(..).ToUnit(X.Unit) doesn't necessarily convert back to X so- having a conversion function in the Equals method would seem like a bad idea. As much as I'd like to have the proper value-based-unit-agnostic-equality-comparison-contract, it just doesn't seem like it's possible with the double arithmetic (I personally think that a As for fixing the Doubles- I think the best approach would be to have the strict equality contract (with an exception for Zero) and a Roslyn Analyzer that should generate a warning when 'comparing floating point numbers'. |
Why do I always seem to be asking the hard questions? 😅 I can understand the difficulty in providing a solution that works for all, hence I'm not really asking for a "fix this now" That being said, I will most likely not be the last one to bump into these kind of issues so it might not be a bad idea to do -something- with it. Maybe have some explicit mention of these "issues" in the docs and provide workarounds for the problem? (like my extension method) In our case for example, we decided that |
I don't think it's a big can of worms at all 😃 I'm all for strict equality. The documentation is clear: The following statements must be true for all implementations of the Equals(Object) method. In the list, x, y, and z represent object references that are not null.
Doing any sort of rounding or "near values" in A lot of methods on collections support passing in custom comparison/IComparer implementations. And the hash based collections can use a custom IEqualityComparer. However the default must be strict per the Framework rules. The overloaded Equals can help with the cases such as 1m.Equals(100cm). On a related note, I'm not a fan of an exception for "Zero" quantities. For example, a length of 0 and a force of 0 mean an absence of value, but in temperatures 0F != 0C. In fact, I think we should remove the Zero property entirely. |
I am leaning towards the same. Strict equality. It is the most consistent with the docs you posted, as well as how record types work. My only problem with it, is that
|
I would not take it that far. As long as a quantity has a well defined base unit, which all ours have, there can be a consistent Zero value. For Temperature, it is 0 Kelvins. However, yes, if the special case is implemented as It is easier if it is consistent, with its quirks and warts. I think we all realize by now, that there is no one perfect solution here that isn't going to trip some people up. The best we can do is be very clear in xmldoc and wiki. In summary, I am for:
|
Regardless of the Also, see here:
It is also slightly more performant than |
We can keep the interface, but I think the main argument is to conform to .NET conventions. To avoid value boxing and support equality checks in generic collections is nice on paper, but I can't really see a real world scenario for using strict value+unit equality checks myself. Except maybe trivial examples where the unit is always the same and values are not subject to rounding errors. People should use the overload that allows to specify tolerance or passing in their own EqualityComparer, outside this interface. But enough of that. Can we land on keeping |
That sounds good to me! |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Fixes #180 Merging the v5 release branch. It is still in alpha, but it is functional, nugets are published and there are not many planned breaking changes left. By merging, all efforts moving forward are targeting v5 and this reduces friction: - No more merge conflicts trying to forward port all changes to v5, instead cherry pick new units and fixes to v4 until v5 is fully stable. - Contributors are having trouble building v4 locally due to `net40`, `net47` and Windows Runtime Component targets. ## 💥 Breaking changes Default number format should be CultureInfo.CurrentCulture, not CurrentUICulture (#795) Use CurrentCulture rather than CurrentUICulture (#986) Return `QuantityValue` in `IQuantity` properties instead of `double` (#1074) Return `decimal` in properties of `Power`, `BitRate` and `Information` quantities (#1074) Fix singular name VolumeFlow.MillionUsGallonsPerDay ## 🔥 Removed Remove targets: net40, net47, Windows Runtime Component. Remove `Undefined` enum value for all unit enum types Remove QuantityType enum Remove IQuantity.Units and .UnitNames Remove IQuantity.ToString() overloads Remove IEquatable<T> and equality operators/methods Remove GlobalConfiguration Remove obsolete and deprecated code. Remove Molarity ctor and operator overloads Remove MinValue, MaxValue per quantity due to ambiguity Remove string format specifiers: "v", "s" json: Remove UnitsNetJsonConverter ## ✨ New QuantityValue: Implement IEquality, IComparable, IFormattable QuantityValue: 16 bytes instead of 40 bytes (#1084) Add `[DataContract]` annotations (#972) ## ♻️ Improvements Upgrade CodeGen, tests and sample apps to net6.0. ## 📝 JSON unit definition schema changes Rename `BaseType` to `ValueType`, for values "double" and "decimal". Rename `XmlDoc` to `XmlDocSummary`. ## TODO Add back `IEquatable<T>`, but implement as strict equality with tuple of quantity name + unit + value. #1017 (comment) ## Postponed for later #1067
Fixes #1017 - Reverted removing `IEquatable`. - Changed the implementation to strict equality on both `Value` and `Unit`. - Improved tests. - Improved xmldocs for equality members. - `GetHashCode()` left unchanged, which includes quantity name in addition to value and unit, so that `LengthUnit.Meter = 1` and `MassUnit.Gram = 1` are still considered different in collections of `IQuantity`. Reverts commit f3c7e25. "🔥 Remove IEquatable<T> and equality operators/methods"
Describe the bug
.GetHashCode()
returns different hash for 2 units for which.Equals()
return trueTo Reproduce
Expected behavior
The
GetHashCode()
should return the same value because the .Equals() returns trueAdditional context
From the remarks:
I've peeked in the sources and I think the issue is, that the value from which the unit is constructed is used when calculating the hashcode:
UnitsNet/UnitsNet/GeneratedCode/Quantities/Length.g.cs
Lines 1025 to 1032 in 7f67ffb
Shouldn't this be normalized to the unit's base unit or something along those lines?
Reason I'm asking is because I'm doing
IEquatable
implementations on my objects and in the.Equals(...)
method, I'm comparing Units to a max of 5 decimals while in theGetHashCode()
method I'm simply using theGetHashCode()
method from the unit.During the PR someone pointed out that for the
GetHashCode()
, the unit should also be rounded using the same logic as in the.Equals(...)
method:The text was updated successfully, but these errors were encountered: