Search code examples
c#dictionarytuplesmultikey

c# Multikey Dictionary, "KeyNotFoundException" when replacing Tuple by own mutable class


I declared a mutable class which serves as a replacement for a tuple as key for a dictionary. The reason is serialization. Serialization works just fine. The following problem occurs, I get a "KeyNotFoundException" when using this class, but only if a new instance of this class is used for the look-up. To make this more clear, see the following class definition:

    public class STuple<T1, T2>    {
    public T1 Item1 { get; set; }
    public T2 Item2 { get; set; }


    public static implicit operator Tuple<T1, T2>(STuple<T1, T2> st)
    {
        return Tuple.Create(st.Item1, st.Item2);
    }

    public static implicit operator STuple<T1, T2>(Tuple<T1, T2> t)
    {
        return new STuple<T1, T2>()
        {
            Item1 = t.Item1,
            Item2 = t.Item2,
        };
    }

    public STuple()
    {
    }
    public STuple(T1 t1, T2 t2) : this()
    {
        Item1 = t1;
        Item2 = t2;
    }
}

And here is the sample program:

        Dictionary<Tuple<string, string>, double> TupleDic = new Dictionary<Tuple<string, string>, double>();
        TupleDic.Add(new Tuple<string, string>("Name1", "Name2"), 5);
        TupleDic.Add(new Tuple<string, string>("Name3", "Name4"), 10);

        Console.WriteLine("dict-Entry 1: {0}", TupleDic[new Tuple<string, string>("Name1", "Name2")]);
        Console.WriteLine("dict-Entry 2: {0}", TupleDic[new Tuple<string, string>("Name3", "Name4")]);


        Dictionary<STuple<string, string>, double> STupleDic = new Dictionary<STuple<string, string>, double>();
        STuple<string, string> STuple1 = new STuple<string, string>("Name1", "Name2");
        STuple<string, string> STuple2 = new STuple<string, string>("Name3", "Name4");
        STupleDic.Add(STuple1, 5);
        STupleDic.Add(STuple2, 10);


        //Still working
        Console.WriteLine();
        Console.WriteLine("Sdict-Entry 1: {0}", STupleDic[STuple1]);
        Console.WriteLine("Sdict-Entry 2: {0}", STupleDic[STuple2]);

        //Not working
        STuple<string, string> STuple3 = new STuple<string, string>("Name1", "Name2");
        STuple<string, string> STuple4 = new STuple<string, string>("Name3", "Name4");
        Console.WriteLine();
        Console.WriteLine("Sdict-Entry 1: {0}", STupleDic[STuple3]);
        Console.WriteLine("Sdict-Entry 2: {0}", STupleDic[STuple4]);

        Console.ReadKey();

The example using the normal tuple works just fine, but when I use my own class STuple it only works, if I use the exact same keys (same instance) as used for adding. I am a total beginner, is it possible that there is a problem because of some mixup with value-type and reference-type?

Really strange in my opinion, a look-up with foreach still works:

            Console.WriteLine();
        foreach (KeyValuePair<STuple<string, string>, double> s in STupleDic)
        {
            Console.WriteLine("Sdict-Entry 1: {0}", s.Value);
        }

Solution

  • CAVEAT: Implementing GetHashCode on a mutable structure is a recipe for disaster. Hashcodes have only one purpose, and that is to facilitate storage in hash-tables. Items used as keys in hash-tables should not mutate (any properties used to calculate the hash) as changing the hashcode causes irrecoverable corruption to the hash-table.

    In order for items to work in hash-table like collections, they must implement equality and hashcode members. As such, you could (with thanks to Resharper):

    public class STuple<T1, T2>
    {
        public STuple()
        {
        }
    
        public STuple(T1 t1, T2 t2)
            : this()
        {
            Item1 = t1;
            Item2 = t2;
        }
    
        public T1 Item1 { get; set; }
        public T2 Item2 { get; set; }
    
        protected bool Equals(STuple<T1, T2> other)
        {
            return EqualityComparer<T1>.Default.Equals(Item1, other.Item1) &&
                   EqualityComparer<T2>.Default.Equals(Item2, other.Item2);
        }
    
        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((STuple<T1, T2>) obj);
        }
    
        public override int GetHashCode()
        {
            unchecked
            {
                return (EqualityComparer<T1>.Default.GetHashCode(Item1)*397) ^
                       EqualityComparer<T2>.Default.GetHashCode(Item2);
            }
        }
    
        public static bool operator ==(STuple<T1, T2> left, STuple<T1, T2> right)
        {
            return Equals(left, right);
        }
    
        public static bool operator !=(STuple<T1, T2> left, STuple<T1, T2> right)
        {
            return !Equals(left, right);
        }
    
    
        public static implicit operator Tuple<T1, T2>(STuple<T1, T2> st)
        {
            return Tuple.Create(st.Item1, st.Item2);
        }
    
        public static implicit operator STuple<T1, T2>(Tuple<T1, T2> t)
        {
            return new STuple<T1, T2>
            {
                Item1 = t.Item1,
                Item2 = t.Item2
            };
        }
    }