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.

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Mar 10, 2016 at 21:28
\$\endgroup\$
2
  • \$\begingroup\$ "and below" - but you're targeting 3.5... is it a requirement that the code should work on 2.0? \$\endgroup\$ Commented Mar 10, 2016 at 22:18
  • 1
    \$\begingroup\$ no, 3.5 is good enough. updated \$\endgroup\$ Commented Mar 10, 2016 at 23:15

2 Answers 2

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);
    
answered Mar 11, 2016 at 7:33
\$\endgroup\$
2
  • \$\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\$ Commented Mar 11, 2016 at 20:42
  • 1
    \$\begingroup\$ Did you read about providing an IEqualityComparer<TKey>as ctor arg for the dictionary? \$\endgroup\$ Commented Mar 11, 2016 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))
 {
 }
 }
answered Mar 12, 2016 at 0:23
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.