4
\$\begingroup\$

I use .NET 3.5 framework, so no tuples. I have many use cases where I have to create a custom key for a dictionary. How can I make this better?

public class CompositeKey<T>
{
    public T Content { get; set; }
    public Func<T, object>[] Lambdas { get; set; }

    public CompositeKey(T obj, params Func<T, object>[] propLambdas)
    {
        Content = obj;
        Lambdas = propLambdas;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        foreach (var l in Lambdas)
        {
            hash ^= l(Content).GetHashCode();
        }
        return hash;
    }

    public override bool Equals(object obj)
    {
        bool isEqual = true;
        if (obj is T)
        {
            T o = (T)obj;
            foreach (var l in Lambdas)
            {
                isEqual &= (l(Content) == l(o));
            }
            return isEqual;
        }
        return false;
    }
}

public class TestCode
{
    public void somemethod()
    {
        Dictionary<CompositeKey<TestPOCO>, string> dict = new Dictionary<CompositeKey<TestPOCO>, string>();
        var t1 = new TestPOCO() { ID=1, Name="A" };
        var t2 = new TestPOCO() { ID = 2, Name = "B" };
        dict.Add(new CompositeKey<TestPOCO>(t1, x => x.ID, x => x.Name) , t1.Name);
        dict.Add(new CompositeKey<TestPOCO>(t2, x => x.ID, x => x.Name), t2.Name);
    }
}

public class TestPOCO
{
    public int ID;
    public string Name;
}

Especially this part:

        dict.Add(new CompositeKey<TestPOCO>(t1, x => x.ID, x => x.Name) , t1.Name);
        dict.Add(new CompositeKey<TestPOCO>(t2, x => x.ID, x => x.Name), t2.Name);

The challenge is to have lambda defined only once, but have different key objects based on the POCO.

| improve this question | | | | |
\$\endgroup\$
  • \$\begingroup\$ "and below" - but you're targeting 3.5... is it a requirement that the code should work on 2.0? \$\endgroup\$ – Mathieu Guindon Mar 10 '16 at 22:18
  • 1
    \$\begingroup\$ no, 3.5 is good enough. updated \$\endgroup\$ – MD Luffy Mar 10 '16 at 23:15
3
\$\begingroup\$
  • Mutable keys are a very bad idea, try this:

    public void Foo()
    {
        var mutable = new Mutable { Value = 1 };
        var dictionary = new Dictionary<Mutable, string> { { mutable, "one" } };
        mutable.Value = 2;
        var explodesHere = dictionary[mutable];
    }
    
    public class Mutable
    {
        public int Value;
    
        public override int GetHashCode()
        {
            return Value;
        }
    }
    
  • GetHashCode must not change while the instance is used in a collection that relies on the value. There are two ways to solve this:

    1. Calculate the hash from values that cannot change. Prefer this wherever possible.
    2. Make sure that the values affecting the hash are not changed while the instance is used in a collection. This is a pit of failure.
  • You can create a dictionary passing in an IEqualityComparer<TKey>

  • About your example:

    ID reads like a property that uniquely identifies an entity and does not change. If this is the case it is a nice candidate to use as a key. I'm guessing a bit here but I'm getting a feeling you really want:

    var idNameMap = new Dictionary<int, string>();
    idNameMap.Add(t1.ID, t1.Name);
    
| improve this answer | | | | |
\$\endgroup\$
  • \$\begingroup\$ no, I used a simplified example. In my usecase, I have to scan through rows of excel sheet. The data in excel is denormalized but I have to normalize it when I store it in the DB. So to detect duplicates, I have to lookup the dictionary with composite values and resolve it to its corresponding ID when present. That ID will be used in a foreign key column. So the question really is "how to use composite keys in dictionary". \$\endgroup\$ – MD Luffy Mar 11 '16 at 20:42
  • 1
    \$\begingroup\$ Did you read about providing an IEqualityComparer<TKey>as ctor arg for the dictionary? \$\endgroup\$ – Johan Larsson Mar 11 '16 at 20:58
0
\$\begingroup\$

I figured out a much simpler way based on IEqualityComparer (thanks to Johan Larsson). I inherited from the dictionary, passed in the lambdas in the constructor. I created a generic comparer. Inside the custom dictionary, I create an instance of the generic comparer and pass in the lambdas. Its much cleaner now.

 CompositeDictionary<TestPOCO, string> cdict = new CompositeDictionary<TestPOCO, string>(x => x.ID, x => x.Name); 

Its pretty straightforward from this point onwards. You just pass in the poco for key. The comparison is all done behind the scene and abstracted away from the user.

Here is the code for the composite dictionary:

class CompositeEqualityComparer<T> : IEqualityComparer<T>
    {
        public Func<T, object>[] Lambdas { get; set; }

        public CompositeEqualityComparer(params Func<T, object>[] propLambdas)
        {
            Lambdas = propLambdas;
        }
        public bool Equals(T obj1, T obj2)
        {
            bool isEqual = true;
            foreach (var p in Lambdas)
            {
                isEqual &= p(obj1).Equals(p(obj2));
                if (!isEqual)
                    return false;
            }
            return isEqual;
        }

        public int GetHashCode(T obj)
        {
            int hCode = 0;
            foreach (var p in Lambdas)
            {
                hCode ^= p(obj).GetHashCode();
            }
            return hCode;
        }
    }

    public class CompositeDictionary<TKey, TValue> : Dictionary<TKey, TValue>
    {
        public CompositeDictionary(params Func<TKey, object>[] props) : base(new CompositeEqualityComparer<TKey>(props))
        {
        }
    }
| improve this answer | | | | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.