I'm looking for real world best practice, how other people might have implented solutions with complex domains
This is what MSDN has to say about IEqualityComparer (non-generic):
This interface allows the implementation of customized equality comparison for collections. That is, you can create your own definition of equality, and specify that this definition be used with a collection type that accepts the
IEqualityComparer
interface. In the .NET Framework, constructors of theHashtable
,NameValueCollection
, andOrderedDictionary
collection types accept this interface.This interface supports only equality comparisons. Customization of comparisons for sorting and ordering is provided by the
IComparer
interface.
It looks like the generic version of this interface performs the same function but is used for Dictionary<(Of <(TKey, TValue>)>)
collections.
As far as best practices around using this interface for your own purposes. I would say that the best practice would be to use it when you are deriving or implementing a class that has similar functionality to the above mentioned .NET framework collections and where you want to add the same capability to your own collections. This will ensure that you are consistent with how the .NET framework uses the interface.
In other words support the use of this interface if you are developing a custom collection and you want to allow your consumers to control equality which is used in a number of LINQ and collection related methods (eg. Sort).
I would say that the best use would be when you need to plug in different equality rules for a certain algorithm. Much in the same way that a sorting algorithm might accept an IComparer<T>
, a finding algorithm might accept an IEqualityComparer<T>
The list uses this interface alot, so you can say a.Substract(b) or other of these nice functions.
Just remember: If you're objects don't return the same Hashcode, the Equals is not called.
I did the following, I'm not sure if it is the world best practice but it worked fine to me :)
Edit: (correct support for custom hash code evaluation required for better linq Distinct support)
public class GenericEqualityComparer<T> : IEqualityComparer<T>
{
private Func<T, T, Boolean> _comparer;
private Func<T, int> _hashCodeEvaluator;
public GenericEqualityComparer(Func<T, T, Boolean> comparer)
{
_comparer = comparer;
}
public GenericEqualityComparer(Func<T, T, Boolean> comparer, Func<T, int> hashCodeEvaluator)
{
_comparer = comparer;
_hashCodeEvaluator = hashCodeEvaluator;
}
#region IEqualityComparer<T> Members
public bool Equals(T x, T y)
{
return _comparer(x, y);
}
public int GetHashCode(T obj)
{
if(obj == null) {
throw new ArgumentNullException("obj");
}
if(_hashCodeEvaluator == null) {
return obj.GetHashCode();
}
return _hashCodeEvaluator(obj);
}
#endregion
}
Then you can use it in your collections.
var comparer = new GenericEqualityComparer<ShopByProduct>((x, y) => { return x.ProductId == y.ProductId; });
var current = SelectAll().Where(p => p.ShopByGroup == group).ToList();
var toDelete = current.Except(products, comparer);
var toAdd = products.Except(current, comparer);
If you need to support custom GetHashCode() functionality, use the alternative contructor to provide a lambda to do the alternative calculation.
e.g.
var comparer = new GenericEqualityComparer<ShopByProduct>(
(x, y) => { return x.ProductId == y.ProductId; },
(x) => { return x.Product.GetHashCode()}
);
I hope this helps =) DMenT
Any time you consider using an IEqualityComparer<T>
, pause to think if the class could be made to implement IEquatable<T>
instead. If a Product
should always be compared by ID, just define it to be equated as such so you can use the default comparer.
That said, there are still a few of reasons you might want a custom comparer:
- If there are multiple ways instances of a class could be considered equal. The best example of this is a string, for which the framework provides six different comparers in
StringComparer
. - If the class is defined in such a way that you can't define it as
IEquatable<T>
. This would include classes defined by others and classes generated by the compiler (specifically anonymous types, which use a property-wise comparison by default).
If you do decide you need a comparer, you can certainly use a generalized comparer (see DMenT's answer), but if you need to reuse that logic you should encapsulate it in a dedicated class. You could even declare it by inheriting from the generic base:
class ProductByIdComparer : GenericEqualityComparer<ShopByProduct>
{
public ProductByIdComparer() : base((x, y) => { return x.ProductId == y.ProductId; })
{ }
}
As far as use, you should take advantage of comparers when possible. For example, rather than calling ToLower()
on every string used as a dictionary key (logic for which will be strewn across your app), you should declare the dictionary to use a case-insensitive StringComparer
. The same goes for the LINQ operators that accept a comparer. But again, always consider if the equatable behavior that should be intrinsic to the class rather than defined externally.