Search code examples
c#.neticomparer

C#: How to evaluate multipe Comparer classes for an object when checking equality?


Motivation

I'm looking to implement IComparer<> in a similar way to the demo code below. Where Foo is the type of objects I need to compare. It does not implement IComparable, but I'm providing an IComparer class for each field so the user can elect to equate to instances based on one field value.

enum Day {Sat, Sun, Mon, Tue, Wed, Thu, Fri};

class Foo {
    public int Bar;
    public string Name;
    public Day Day;
}

Comparer classes are:

// Compares all fields in Foo
public class FooComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Bar == y.Bar && x.Name == y.Name && return x.Day == y.Day;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

// Compares only in Foo.Bar
public class FooBarComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Bar == y.Bar;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

// Compares only in Foo.Name
public class FooNameComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Name == y.Name;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

// Compares only in Foo.Day
public class FooDayComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y)) return true;
        return x.Day == y.Day;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hashCode = obj.Bar;
            hashCode = (hashCode * 397) ^ (obj.Name != null ? obj.Name.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (int) obj.Day; 0);
            return hashCode;
        }
    }
}

Question

I want to allow the user to be able to combine multiple Comparer types to evaluate two instances of Type Foo. I'm not sure how to do that.

Idea

What I came up with is something like this, where I AND the results of comparisons done by all comparers in the list:

bool CompareFoo(Foo a, Foo b, params IComparer[] comparers)
{
    bool isEqual = true;
    // Or the list and return;
    foreach (var comparer in comparers)
    {
        isEqual = isEqual && comparer.Equals(x,y);
    }
    return isEqual;
}

Notes

  • My target .NET version is 4.5.
  • I may be stuck with C# 5.0.
  • Also, may be stuck with `MSBuild 12.0
  • This is my first time to use IComparer.

Solution

  • It seems to me that what you are trying to achieve feels very similar to a Chain-of-responsibility.

    So why don't you arrange all your Foo comparers in a chain-like structure and make the chain extensible so that new links can be added at run-time?

    Here's is the idea:

    the client will implement whatever Foo comparers it wants and all of them will be neatly arranged in a way that all of them will be called one by one and if anyone returns false then the whole comparison returns false!

    Here's the code:

    public abstract class FooComparer
    {
        private readonly FooComparer _next;
    
        public FooComparer(FooComparer next)
        {
            _next = next;
        }
    
        public bool CompareFoo(Foo a, Foo b)
        {
            return AreFoosEqual(a, b) 
                && (_next?.CompareFoo(a, b) ?? true);
        }
    
        protected abstract bool AreFoosEqual(Foo a, Foo b);
    }
    
    public class FooNameComparer : FooComparer
    {
        public FooNameComparer(FooComparer next) : base(next)
        {
        }
    
        protected override bool AreFoosEqual(Foo a, Foo b)
        {
            return a.Name == b.Name;
        }
    }
    
    public class FooBarComparer : FooComparer
    {
        public FooBarComparer(FooComparer next) : base(next)
        {
        }
    
        protected override bool AreFoosEqual(Foo a, Foo b)
        {
            return a.Bar == b.Bar;
        }
    }
    

    The idea of the FooComparer abstract class is having something like a chain manager; it handles the calling of the whole chain and forces its derived classes to implement the code to compare the Foo's, all while exposing the method CompareFoo which is what the client will use.

    And how will the client use it? well it can do something like this:

    var manager = new FooManager();
    
    manager.FooComparer
        = new FooNameComparer(new FooBarComparer(null));
    
    manager.FooComparer.CompareFoo(fooA, fooB);
    

    But it's cooler if they can register the FooComparer chain to the IoC Container!

    Edit

    This is a more simplistic approach I've been using for a while to custom compare things:

    public class GenericComparer<T> : IEqualityComparer<T> where T : class
    {
        private readonly Func<T, object> _identitySelector;
    
        public GenericComparer(Func<T, object> identitySelector)
        {
            _identitySelector = identitySelector;
        }
        public bool Equals(T x, T y)
        {
            var first = _identitySelector.Invoke(x);
            var second = _identitySelector.Invoke(y);
    
            return first != null && first.Equals(second);
        }
        public int GetHashCode(T obj)
        {
            return _identitySelector.Invoke(obj).GetHashCode();
        }
    }
    
    public bool CompareFoo2(Foo a, Foo b, params IEqualityComparer<Foo>[] comparers)
    {
        foreach (var comparer in comparers)
        {
            if (!comparer.Equals(a, b))
            {
                return false;
            }
        }
    
        return true;
    }
    

    And let the client do:

    var areFoosEqual = CompareFoo2(a, b, 
    new GenericComparer<Foo>(foo => foo.Name), 
    new GenericComparer<Foo>(foo => foo.Bar))
    

    It may be possible to adapt the GenericComparer to have multiple identity selector as to pass them all in a single lambda but we would also need to update its GetHashCode method to compute the HashCode correctly using all the identity objects.