I was writing an answer in another thread and came across this challenge. I'm trying to have a generic class so that I can delegate the routine (and tiring) Equals
and GetHashCode
implementation to it (which should handle all that). With the code things will be clearer.
Please note that I have no problem with performance as of now. But when writing a generic library for future use, I'm contemplating better designs. I'll include the bare minimum code required to drive the idea.
Approach 1
public class Equater<T> : IEqualityComparer<T>
{
public IEnumerable<Func<T, object>> Keys { get; private set; }
public Equater(params Func<T, object>[] keys)
{
Keys = keys;
}
public bool Equals(T x, T y)
{
----
}
public int GetHashCode(T obj)
{
.....
}
}
//an example usage
public class Dao : IEquatable<Dao>
{
static Equater<Dao> equater = new Equater<Dao>(x => x.Id, x => x.Table);
public bool Equals(Dao other)
{
return equater.Equals(this, other);
}
public override int GetHashCode()
{
return equater.GetHashCode(this);
}
}
This is great considering it works for any number of properties. But performance sucks, boxing I believe is the culprit. Runs in around 260 ms for about 100000 calls to GetHashCode
.
Approach 2
public static class Equater<T>
{
public static Func<T, T, bool> equals;
public static Func<T, int> getHashCode;
public static void Set<TKey1, TKey2>(Func<T, TKey1> key1Selector,
Func<T, TKey2> key2Selector)
{
equals = (x, y) =>
{
----
};
getHashCode = t => { .... };
}
//other overloads of Set with varying type arguments
}
//an example usage
public class Dao : IEquatable<Dao>
{
static Dao()
{
Equater<Dao>.Set(x => x.Id, x => x.Table);
}
public bool Equals(Dao other)
{
return Equater<Dao>.equals(this, other);
}
public override int GetHashCode()
{
return Equater<Dao>.getHashCode(this);
}
}
Runs in around 100 - 110 ms this time.
Approach 3
public abstract class Equater<T> : IEqualityComparer<T>
{
public static Equater<T> Create<TKey>(Func<T, TKey> keySelector)
{
return new Impl<TKey>(keySelector);
}
public static Equater<T> Create<TKey1, Tkey2>(Func<T, TKey1> key1Selector,
Func<T, Tkey2> key2Selector)
{
return new Impl<TKey1, Tkey2>(key1Selector, key2Selector);
}
//etc. other overloads of Create with varying type arguments
public abstract bool Equals(T x, T y);
public abstract int GetHashCode(T obj);
class Impl<TKey> : Equater<T>
{
readonly Func<T, TKey> keySelector;
public Impl(Func<T, TKey> keySelector)
{
this.keySelector = keySelector;
}
public override bool Equals(T x, T y)
{
----
}
public override int GetHashCode(T obj)
{
....
}
}
class Impl<TKey1, TKey2> : Equater<T>
{
readonly Func<T, TKey1> key1Selector;
readonly Func<T, TKey2> key2Selector;
public Impl(Func<T, TKey1> key1Selector, Func<T, TKey2> key2Selector)
{
this.key1Selector = key1Selector;
this.key2Selector = key2Selector;
}
public override bool Equals(T x, T y)
{
----
}
public override int GetHashCode(T obj)
{
....
}
}
}
//an example usage
public class Dao : IEquatable<Dao>
{
static Equater<Dao> equater = Equater<Dao>.Create(x => x.Id, x => x.Table);
public bool Equals(Dao other)
{
return equater.Equals(this, other);
}
public override int GetHashCode()
{
return equater.GetHashCode(this);
}
}
Approaches 80 - 90 ms. This is a lot verbose since I have to write a class for every additional type argument, but is the fastest.
Can I implement it better?
Please note that my question is not about performance as such (for that reason I've not included the critical GetHashCode
part as my question is not about that). I'm trying to know if I can implement the above in a cooler and more efficient way. In other words its more about efficiency than performance.