2

I was scratching my head over an unexpected failing comparison between two instances of a custom struct. I hope someone can either point me to reference source or documentation, or confirm whether this is a bug!

Distilling the issue gets something like the following:

public readonly struct TwoDecimals
{
    public readonly decimal First;
    public readonly decimal Second;
    public TwoDecimals(decimal first, decimal second)
    {
        this.First = first;
        this.Second = second;
    }
}

public void Main()
{
    var withoutTrailingZero = new TwoDecimals(42m, 42m);
    var withTrailingZero = new TwoDecimals(42.0m, 42m);
    var equal = withoutTrailingZero.Equals(withTrailingZero); // true
    var equalHashCodes = withoutTrailingZero.GetHashCode() == withTrailingZero.GetHashCode(); // false!
}

This is a problem, because if Equals is true, GetHashCode should definitely return the same value.

I'm not overriding either of Equals or GetHashCode, so the custom struct should get the default ValueType implementation, which may be platform-specific. But reading in the reference source here I see a comment with this claim:

Our algorithm for returning the hashcode is a little bit complex. We look for the first non-static field and get it's [sic] hashcode.

Evidently that is not true, since 42.0m and 42m have the same hashcode, despite having different bits.

It seems as if, instead, it is falling back to using the bits for some reason. Unfortunately that method is extern and I'm not sure how to find the/a source.

The reference source does have an implementation of Equals, which uses reflection to compare each field of the struct. But first, it does this check

// if there are no GC references in this object we can avoid reflection 
// and do a fast memcmp
if (CanCompareBits(this))
    return FastEqualsCheck(thisObj, obj);

FastEqualsCheck, presumably, just compares the bits. In my case, it must not be using this fast check, because the bits are definitely different, but I suspect that something similar may be happening in the implementation for GetHashCode.


I also tried analogous cases using doubles (0.0d and -0.0d which have different bits but the same hashcode and are considered 'equal') and floats. The behaviour still wasn't as expected, but in a different way! This time, the hashcodes were once again different (and not, as suggested in that code comment, equal to the hashcode of the first field). But this time, the Equals method returns false, so at least it is consistent.

Oly
  • 2,329
  • 1
  • 18
  • 23
  • 1
    This answer https://stackoverflow.com/a/3842515/5181199 seems to partially explain what's going on, although it's outside the scope of the given question! – Oly Sep 14 '18 at 14:00
  • 2
    That answer seems to explicitly answer this one, particularly the last part. Given that answer - basically it's using a bitwise representation, which is a bug, I'm not sure what else could be added to an answer to *this* question. Of course the preferred fix here is to override Equals/GetHashCode yourself and also implement `IEquatable`, but I do appreciate the curiosity. – Jon Skeet Sep 14 '18 at 14:13
  • Also see https://stackoverflow.com/questions/5926776/how-does-native-implementation-of-valuetype-gethashcode-work – Matthew Watson Sep 14 '18 at 14:25
  • It seems like the answer to this question is hidden as a 'bonus fun fact' in answers to other related questions. I'd love to see some real documentation, or a known bug note. – Oly Sep 14 '18 at 21:43

0 Answers0