-1

A few days ago, I asked a question about dictionary implementations in C# for a class I had written. I have a dictionary that has keys of class Interval and values of class Line2D. My updated implementation of interval looks like this:

public class Interval : ICloneable, IComparable, IComparable<Interval>, IComparable<double>
{
    public Interval()
    {

    }
    public Interval(Interval interval)
    {
        this.CopyFrom<Interval>(interval);
    }
    public Interval(double start, double end)
    {
        Start = start;
        End = end;
    }

    // Properties
    public double Start { get; set; } = double.NaN;
    public double End { get; set; } = double.NaN;
    public double Span => End - Start;

    // Methods
    public object Clone() => MemberwiseClone();

    public int CompareTo(object obj)
    {
        if (obj is Interval iObj)
        {
            return CompareTo(iObj);
        }
        return 1;
    }

    public int CompareTo([AllowNull] Interval other)
    {
        if (Start == other.Start && End == other.End)
        {
            return 0;
        }
        else if (End <= other.Start)
        {
            return -1;
        }
        else if (other.End <= Start)
        {
            return 1;
        }
        else
        {
            throw new ArgumentException("Interval must not overlap with this one.", nameof(other));
        }
        // Old implementation
        //if (Start < other.Start)
        //{
        //    return -1;
        //}
        //else if (Start > other.Start)
        //{
        //    return 1;
        //}
        //else
        //{
        //    return 0;
        //}
    }

    public int CompareTo([AllowNull] double other)
        => Contains(other) ? 0 : (other < Start ? 1 : -1);

    public bool Contains(double x) => Start <= x && x <= End;
    public override string ToString() => $"[{Start}, {End}]";
}

So if I create a dictionary where the keys are Interval objects, I thought my CompareTo methods would cover the situation where two intervals have the same starting point and end point. However, this is not the case.

var testDict = new Dictionary<Interval, int>();
var testInterval1 = new Interval(0, 1);
var testInterval2 = new Interval(testInterval1); // Should be identical
testDict[testInterval1] = 5;
var contains = testDict.ContainsKey(testInterval2); // This is false when it should be true;
testDict[testInterval2] = 10; // This shouldn't work but it does

Why doesn't the default comparer hop into my CompareTo methods during execution?

JansthcirlU
  • 688
  • 5
  • 21

4 Answers4

1

Because Dictionary doesn't use IComparer<T>, it uses IEqualityComparer<T>.

From the Remarks section of the official documentation of Dictionary<TKey,TValue>:

Dictionary<TKey,TValue> requires an equality implementation to determine whether keys are equal. You can specify an implementation of the IEqualityComparer<T> generic interface by using a constructor that accepts a comparer parameter; if you do not specify an implementation, the default generic equality comparer EqualityComparer<T>.Default is used. If type TKey implements the System.IEquatable<T> generic interface, the default equality comparer uses that implementation.

Note that it's not your type that needs to implement the IEqualityComparer<T> interface - but instead, it can be passed as a dependency to the Dictionary's constructor.

If you want your type to be compared in a specific way, you should implement the IEquatable<T> interface in your type.

Note: If you implement the IEquatable<T> interface, it's recommended to also override the Equals(object) and GetHashCode() methods, and overload the == and != operators, as explained in Notes to Implementers:

Replace the type parameter of the IEquatable<T> interface with the type that is implementing this interface. If you implement IEquatable<T>, you should also override the base class implementations of Equals(Object) and GetHashCode() so that their behavior is consistent with that of the Equals(T) method. If you do override Equals(Object), your overridden implementation is also called in calls to the static Equals(System.Object, System.Object) method on your class. In addition, you should overload the op_Equality and op_Inequality operators. This ensures that all tests for equality return consistent results.

Zohar Peled
  • 79,642
  • 10
  • 69
  • 121
0

To use complex values as dictionary keys, you need to specifically implement GetHashCode() and Equals(). CompareTo is not relevant for this use-case.

spender
  • 117,338
  • 33
  • 229
  • 351
  • 1
    Not only were you first, the others suggested using `IEquatable` which in itself was not in enough in my case. Implementing both `Equals` **and** `GetHashCode` is what worked for me. – JansthcirlU Aug 20 '20 at 11:59
0

I think Dictionary uses Equals and GetHashCode methods of the class, so you need to override those. See this question

DeepakP
  • 81
  • 1
  • 4
0

Comparable != equatable

Equatable checks are able to tell you if two items are equal or not. Comparable checks are able to tell you which item ranks higher than the other.

While comparability inherently requires equatability (because if you can't tell things apart, you cannot rank them), equatability does not require comparability. Since the dictionary only cares about equating its key values, it completely ignores whether your Interval class is comparable or not.

A dictionary contains its own equality comparer. By default, it uses the default equality comparer, and the default equality comparer relies on IEquatable<T>, not IComparable<T>.

Note
MSDN is incorrect in its claim that it uses IEquatable<T>.Equals to check for equality. When you test this with a dictionary, it's in fact IEquatable<T>.GetHashCode that is used to check equality between dictionary key values.

In other words: a dictionary checks if key values are (not) equal to each other, it does not attempt to order/rank the key values.

There are two possible solutions here:

  • Implement IEquatable<T> on your Interval class and rely on the dictionary's default equality comparer
  • Give your dictionary a custom equality comparer where you can define your own equality comparison logic (MSDN link)
Flater
  • 12,908
  • 4
  • 39
  • 62
  • I don't think implementing `IEquatable` is enough since a dictionary will look at the result of `GetHashCode()` first. If that one isn't overwritten in a way that will result in the same value for objects that are equal `IEquatable` won't do anything –  Aug 20 '20 at 11:36
  • @Knoop: Your claim contradicts MSDN documentation, cfr the first link I provided: _"[The dictionary's default equality comparer] in turn uses the implementation of the `IEquatable.Equals` method in the Box class."_ However, the code example does also implement `GetHashCode` as well. I'd suggest implementing it anyway for good measure, but the documentation claims that it is not how a dictionary approaches equality (by default) – Flater Aug 20 '20 at 11:42
  • @Knoop: Sorry, the link I reference talks about the default equality comparer, not the dictionary itself. [Here is the link](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.comparer?view=netcore-3.1) that confirms that the default equality comparer (from my initial link) is being used in the dictionary. I've also fixed the answer to provide both links. – Flater Aug 20 '20 at 11:47
  • @Knoop: I also tested and confirmed the MSDN example code it using dotnetfiddle ([link](https://dotnetfiddle.net/08ys3R)). Whether I override GetHashCode or keep it default, the outcome remains the same. Therefore, the implementation of `GetHashCode` isn't what drives the equality comparer to find an equality. – Flater Aug 20 '20 at 11:53
  • For me, only implementing `IEquatable` didn't work, I had to explicitly override both `Equals(object obj)` and `GetHashCode()` before it would actually yield the right outcome. But thanks! – JansthcirlU Aug 20 '20 at 11:57
  • @Flater you created a fiddle that doesn't even use a dictionary.Here is your example using a dictionary that will not find an equal box: https://dotnetfiddle.net/HdycW6. If you change the `GetHashCode()` implementation to the one you created using the properties it will find the `newRedBox` –  Aug 20 '20 at 12:09
  • @Knoop: There wasn't a lot of wiggle room for MSDN's assertion that dictionary use default equality comparers and the code sample which proves how the default equality comparer works. Regardless, it seems you are correct and therefore MSDN is provably wrong. I'll fix the answer accordingly. – Flater Aug 20 '20 at 12:12
  • 1
    @Flater MSDN isn't wrong. A dictionary uses both. First it uses `GetHashCode` to be able to find matches in `O(1)` time complexity (instead of having to loop a list like your fiddle did) and for those matches it uses the provided (or default) equality comparer to see if they are equal (since hash codes can have collisions) –  Aug 20 '20 at 12:14
  • @Knoop: MSDN tells you two things: "By default, a dictionary uses the default equality comparer" (link 1) and "The default equality comparer uses `Equals`" (link 2) (**not** `GetHashCode`, which I confirmed in my fiddle). If MSDN does not in any way point out that the usage of `GetHashCode` (instead of `Equals`) is prioritized in the case of dictionaries using the default equality comparer, then **MSDN is wrong by not acknowledging the dictionary's custom handling of the default equality comparer**. – Flater Aug 20 '20 at 12:35
  • @Flater Read the [remarks](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.equalitycomparer-1.default?view=netcore-3.1#remarks). The note in your answer is incorrect: `GetHashCode` is used first, and if there is a match then `Equals` must return `true`. – Johnathan Barclay Aug 20 '20 at 12:45
  • @JohnathanBarclay: The remarks make no mention of which method is prioritized. Also, themethods explicitly referenced in the remarks are those of `object`, not `IEquatable`. Secondly, on the [`EqualityComparer` MSDN page](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iequalitycomparer-1?view=netcore-3.1) it describes the `EqualityComparer` as "using the `IEquatable.Equals` method" and makes no mention of using `GetHashCode`. I have yet to find a single mention across all related MSDN pages when `GetHashcode` is favored and when `Equals` is favored. – Flater Aug 20 '20 at 14:12
  • @Flater Check the [remarks](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?view=netcore-3.1#remarks) on `Dictionary`: "_Retrieving a value by using its key is very fast, close to O(1), because the Dictionary class is implemented as a hash table_", thus a hash code is required to obtain the location of an object before equality can be checked; that's how a hash table works. `IEqualityComparer` defines a `GetHashCode(T)` method which is used as the hash function. – Johnathan Barclay Aug 20 '20 at 14:27
  • `IEquatable` is a different interface to `IEqualityComparer` as not all structures employ hashing. `Dictionary` requires an `IEqualityComparer`. "_if you do not specify one, the default generic equality comparer EqualityComparer.Default is used_". `EqualityComparer.Default` remarks "_The Default property checks whether type T implements the System.IEquatable interface and, if so, returns an EqualityComparer that uses that implementation. Otherwise, it returns an EqualityComparer that uses the overrides of Object.Equals and Object.GetHashCode provided by T_ – Johnathan Barclay Aug 20 '20 at 14:30
  • @JohnathanBarclay. Show me where in that is says that `GetHashCode` take precedence. _"an EqualityComparer that uses that implementation"_ is much too vague. – Flater Aug 20 '20 at 16:50
  • It doesn't take precedence, and from the consumers perspective it doesn't really matter: both must be satisfied. However in the implementation `GetHashCode` will be called first; it is through an efficient hash function that hash tables achieve their efficiency. It means they don't need to iterate through all items to check for equality; they only check those with a matching hash value. The MSDN documentation definitely isn't wrong here, just maybe not detailed enough for your liking. – Johnathan Barclay Aug 20 '20 at 17:20
  • @Flater there is no precedence. It's impossible to determine equality with `GetHashCode`. However, if it's implemented correctly, you can determine inequality with `GetHashCode`. So if implemented incorrectly items that are equal will never be checked for equality because they have a different hashcode (and per definition of hashcodes should never be equal). That's not "wrong" that's just incorrectly implemented software. –  Aug 20 '20 at 18:30
  • Oh wow, this topic was more controversial than I had imagined! – JansthcirlU Aug 21 '20 at 13:24
  • @JohnathanBarclay: I'm not arguing against using hash codes for equality checks. I'm saying that the documentation explicitly suggests that the `Equals` method is being used (_"[The default equality comparer] in turn uses the implementation of the `IEquatable.Equals` method"_) **without quantifying that this is not always the case**, when in reality it is indeed not always the case, as we've established for dictionaries and the hashmaps on which they rely. – Flater Aug 21 '20 at 13:28