7

I'm trying to create a Dictionary is C# that takes an Unordered Pair of Indices as its Key.

For example:

exampleDictionary[new UnorderedPair(x,y)] and exampleDictionary[new UnorderedPair(y,x)] should both return the same value.

Is there a way to create a custom unordered collection other than using a HashSet? Or some way to create an unordered Tuple?

This question is similar to what I'm trying to accomplish, except in C# rather than python.

FaffyWaffles
  • 125
  • 1
  • 7
  • 1
    Is it so that you would want the x and y to be processed to return the same unique key? Then x + y is same as y + x. If x and y are any type, then you would need to convert them first like if it's string, add x and y hash values so it is sure to be unique. – Everts Oct 13 '22 at 07:08
  • A tuple is not the same as an unordered pair (referring to the other question you provided). The `new` keyword creates a new instance of a class and I don't think that this approach will ever work, because if an object is used as a key, then you need to reuse that same object to access the elements of the Dictionary. What are you even trying to accomplish and why? – Julian Oct 13 '22 at 07:09
  • 1
    @ewerspej It can work; it does not have to be the same object instance. Either you override both `Equals(object)` and `GetHashCode()` inside your `UnorderedPair` type (if you use a struct, also consider implementing `IEquatable`). _Or else_ you create a custom equality comparer (make another class `UnorderedPairEqualityComparer` that has `EqualityComparer` as base class, and send in an instance of this comparer when you create your `exampleDictionary`, instead of relying on the default equality comparer). – Jeppe Stig Nielsen Oct 13 '22 at 09:04

3 Answers3

7

If the type is not your own or you can't or don't want to modify refer to Theodor Zoulias's answer

Otherwise, assuming that UnorderedPair is your own class you can modify what you could do is e.g.

[Serializable]
public class UnorderedPair<T> : IEquatable<UnorderedPair<T>>
{
    public T X;
    public T Y;

    public UnorderedPair()
    {
        
    }

    public UnorderedPair(T x, T y)
    {
        X = x;
        Y = y;
    }

    public bool Equals(UnorderedPair<T> other)
    {
        if (ReferenceEquals(null, other))
        {
            return false;
        }

        if (ReferenceEquals(this, other))
        {
            return true;
        }

        // For equality simply include the swapped check
        return X.Equals(other.X) && Y.Equals(other.Y) || X.Equals(other.Y) && Y.Equals(other.X);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }

        if (ReferenceEquals(this, obj))
        {
            return true;
        }

        if (obj.GetType() != GetType())
        {
            return false;
        }

        return Equals((UnorderedPair<T>)obj);
    }

    public override int GetHashCode()
    {
        // and for the HashCode (used as key in HashSet and Dictionary) simply order them by size an hash them again ^^
        var hashX = X == null ? 0 : X.GetHashCode();
        var hashY = Y == null ? 0 : Y.GetHashCode();
        return HashCode.Combine(Math.Min(hashX,hashY), Math.Max(hashX,hashY));
    }

    public static bool operator ==(UnorderedPair<T> left, UnorderedPair<T> right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(UnorderedPair<T> left, UnorderedPair<T> right)
    {
        return !Equals(left, right);
    }
}

and then e.g.

var testDict = new Dictionary<UnorderedPair<int>, string>();
testDict.Add(new UnorderedPair<int>(1,2), "Hello World!");
Console.WriteLine(testDict[new UnorderedPair<int>(2,1)]);

As per suggestion by Jodrell in the comments you could even make the types swappable - not sure this would be ever needed - but this way you could even have a pair of different types:

[Serializable]
public class UnorderedPair<TX, TY> : IEquatable<UnorderedPair<TX, TY>>
{
    public TX X;
    public TY Y;

    public UnorderedPair()
    {
        
    }

    public UnorderedPair(TX x, TY y)
    {
        X = x;
        Y = y;
    }

    public UnorderedPair(TY y, TX x)
    {
        X = x;
        Y = y;
    }

    public override int GetHashCode()
    {
        // and for the HashCode (used as key in HashSet and Dictionary) simply order them by size an hash them again ^^
        var hashX = X == null ? 0 : X.GetHashCode();
        var hashY = Y == null ? 0 : Y.GetHashCode();
        var combine = HashCode.Combine(Math.Min(hashX, hashY), Math.Max(hashX, hashY));
        return combine;
    }

    public bool Equals(UnorderedPair<TX, TY> other)
    {
        if (ReferenceEquals(null, other))
        {
            return false;
        }

        if (ReferenceEquals(this, other))
        {
            return true;
        }

        if (typeof(TX) != typeof(TY))
        {
            return EqualityComparer<TX>.Default.Equals(X, other.X) && EqualityComparer<TY>.Default.Equals(Y, other.Y);
         
        }
        
        return  EqualityComparer<TX>.Default.Equals(X, other.X) && EqualityComparer<TY>.Default.Equals(Y, other.Y)
            || X.Equals(other.Y) && Y.Equals(other.X);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }

        if (ReferenceEquals(this, obj))
        {
            return true;
        }

        return obj switch
        {
            UnorderedPair<TX, TY> other => Equals(other),
            UnorderedPair<TY, TX> otherSwapped => Equals(otherSwapped),
            _ => false
        };
    }

    public static bool operator ==(UnorderedPair<TX, TY> left, UnorderedPair<TX, TY> right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(UnorderedPair<TX, TY> left, UnorderedPair<TX, TY> right)
    {
        return !Equals(left, right);
    }

    public static implicit operator UnorderedPair<TX, TY>(UnorderedPair<TY, TX> pair)
    {
        return new UnorderedPair<TX, TY>(pair.Y, pair.X);
    }
}

and

var testDict = new Dictionary<UnorderedPair<int, double>, string>();
testDict.Add(new UnorderedPair<int, double>(1,2.5), "Hello World!");
Console.WriteLine(testDict[new UnorderedPair<double,int>(2.5,1)]);

(.NET Fiddle for both)

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Generally the right idea, I think but, shouldn't the type of X and Y be generic and not necessarily the same. – Jodrell Oct 13 '22 at 07:22
  • 1
    @Jodrell generic, yes why not ^^ But `not necessarily the same` would be possible generally but since the `Dictionary` itself is strictly typed anyway it would be no use to be able to compare a `UnorderedPair` to a `UnorderedPair` since the `Dictionary` could anyway only either be of type `Dictionary, OtherType>` or `Dictionary, OtherType>` so afaik you can only use according key type anyway ... or am I wrong here? – derHugo Oct 13 '22 at 07:58
  • Well you could of course add some implicit conversions to it though ^^ But not sure if this is going to be a bit overkill – derHugo Oct 13 '22 at 07:59
  • agreed, different generic types doesn't make much sense – Jodrell Oct 13 '22 at 08:02
  • @Jodrell added it at the bottom via implicit conversion .. not sure if you would ever use it but it can be done ^^ – derHugo Oct 13 '22 at 08:08
  • 1
    The `UnorderedPair` implementation can fail if `TX` and `TY` happen to be the same, although the existence of the "backwards" instance constructor overload makes that less common (but not impossible!). If anybody ever wants something like that, consider a check in a static constructor, like `static UnorderedPair() { if (typeof(TX) == typeof(TY)) throw new NotSupportedException("Types must be different"); }` – Jeppe Stig Nielsen Oct 13 '22 at 09:20
  • 1
    Thanks @JeppeStigNielsen that is really good to know! The two constructors I guess woul actually make it impossible, not declaring the type e.g. `UnorderedPair` but latest when creating an instance it will not compile due to "ambigious constructor" ... would you have an idea for actually allowing that? I suspect OP's main attempt would actually be the upper solution for the same type for both values anyway – derHugo Oct 13 '22 at 10:39
  • 1
    @JeppeStigNielsen treating this case explicitly like e.g. `if (typeof(TX) == typeof(TY)) { return EqualityComparer.Default.Equals(X, other.X) && EqualityComparer.Default.Equals(Y, other.Y) || X.Equals(other.Y) && Y.Equals(other.X); }` seems to do the trick, what do you think? – derHugo Oct 13 '22 at 11:23
  • All I know is with the code you had 24 hours ago, it was not completely impossible to make `UnorderedPair` as you ask for. For example, with a little method `static UnorderedPair GetUnorderedPairWithInt32(TOther o, int i) => new UnorderedPair(o, i);` I could get around the problem with two constructor overloads, and then with `var p = GetUnorderedPairWithInt32(5, 8);` and `GetUnorderedPairWithInt32(8, 5)` I could get the problem I described then. But since you have changed your code now, it is now longer relevant, I believe. – Jeppe Stig Nielsen Oct 15 '22 at 07:26
5

You could write a custom IEqualityComparer<UnorderedPair<T>> implementation, and pass it as argument to the constructor of your Dictionary<UnorderedPair<TKey>, TValue>. This way you won't have to modify your UnorderedPair<T> type, by overriding its Equals and GetHashCode methods. Below is an example of such a comparer for the ValueTuple<T1, T2> struct, with both T1 and T2 being the same type:

class UnorderedValueTupleEqualityComparer<T> : IEqualityComparer<(T, T)>
{
    private readonly IEqualityComparer<T> _comparer;

    public UnorderedValueTupleEqualityComparer(IEqualityComparer<T> comparer = default)
    {
        _comparer = comparer ?? EqualityComparer<T>.Default;
    }

    public bool Equals((T, T) x, (T, T) y)
    {
        if (_comparer.Equals(x.Item1, y.Item1)
            && _comparer.Equals(x.Item2, y.Item2)) return true;
        if (_comparer.Equals(x.Item1, y.Item2)
            && _comparer.Equals(x.Item2, y.Item1)) return true;
        return false;
    }

    public int GetHashCode((T, T) obj)
    {
        int h1 = _comparer.GetHashCode(obj.Item1);
        int h2 = _comparer.GetHashCode(obj.Item2);
        if (h1 > h2) (h1, h2) = (h2, h1);
        return HashCode.Combine(h1, h2);
    }
}

Usage example:

Dictionary<(int, int), string> dictionary = new(
    new UnorderedValueTupleEqualityComparer<int>());
Aaron Bertrand
  • 272,866
  • 37
  • 466
  • 490
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
4

Inspired by @derHugo's answer and my comments on it,

Fiddle here

A generic implementation,

#nullable enable
public class UnorderedPair<T> : IEquatable<UnorderedPair<T>>
{
    private static IEqualityComparer<T> comparer = EqualityComparer<T>.Default;
    
    public T X { get; }
    public T Y { get; }
    
    public UnorderedPair(T x, T y)
    {
        X = x;
        Y = y;
    }

    public bool Equals(UnorderedPair<T>? other)
    {
        if(other is null)
        {
            return false;
        }

        if (ReferenceEquals(this, other))
        {
            return true;
        }
        
        // For equality simply include the swapped check
        return
                comparer.Equals(X, other.X) && comparer.Equals(Y, other.Y)
            ||
                comparer.Equals(X, other.Y) && comparer.Equals(Y, other.X);
    }

    public override bool Equals(object? obj)
    {
        return Equals(obj as UnorderedPair<T>);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return
                    (X is null ? 0 : comparer.GetHashCode(X))
                +
                    (Y is null ? 0 : comparer.GetHashCode(Y));
        }
    }

    public static bool operator ==(UnorderedPair<T>? left, UnorderedPair<T>? right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(UnorderedPair<T>? left, UnorderedPair<T>? right)
    {
        return !Equals(left, right);
    }
}
#nullable disable
Jodrell
  • 34,946
  • 5
  • 87
  • 124