1

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.
mbadawi23
  • 1,029
  • 2
  • 21
  • 43
  • So you want an `IEqualityComparer` that considers two `Foo` as equal if any one of a list of `IEqualityComparer` would consider them equal? – Joe Sewell Sep 15 '18 at 03:35
  • @JoeSewell, the behavior I’m looking for is to allow the user to “layer” conditions for equality of an object. Eventually this will be used query a collection of those objects. – mbadawi23 Sep 15 '18 at 03:45
  • Could you clarify what "layer" means? Let's say we create `combinedComparer`, which combines `comparerA` and `comparerB`. Do you want `combinedComparer.Equals(fooX, fooY)` to be true if **both** `comparerA.Equals(fooX, fooY)` and `comparerB.Equals(fooX, fooY)` are true, or if **either** is true, or something else? – Joe Sewell Sep 15 '18 at 03:49
  • 1
    If both comparers are true, but ignore the field for the comparer not specified. In other words, it’s an `and` operation using `ComparerA` and `ComparerB` only. – mbadawi23 Sep 15 '18 at 03:54
  • 1
    Don't forget to test `Foo x` and `Foo y` for `null` in your comparer. This is Something your examples and some of the answers are omitting. If y is null then `y.Name` will throw an exception! Also I know it's just example but your Day comparer is evaluating Name property – pinkfloydx33 Sep 15 '18 at 09:14

3 Answers3

2

You can combine multiple IEqualityComparer<Foo> objects by defining an additional comparer that takes other comparers as constructor parameters:

public class CompositeFooComparer : IEqualityComparer<Foo>
{
    private IEqualityComparer<Foo>[] comparers;
    public CompositeFooComparer(params IEqualityComparer<Foo>[] comparers)
    {
        this.comparers = comparers;
    }

    public bool Equals(Foo x, Foo y)
    {
        foreach (var comparer in comparers)
        {
            if (!comparer.Equals(x, y))
            {
                return false;
            }
        }
        return true;
    }

    public int GetHashCode(Foo obj)
    {
        var hash = 0;
        foreach (var comparer in comparers)
        {
            hash = hash * 17 + (comparer.GetHashCode(obj));
        }
        return hash;
    }
}

Then you can create and use it like this:

var fooA = new Foo
{
    Bar = 5,
    Day = Day.Fri,
    Name = "a"
};

var fooB = new Foo
{
    Bar = 5,
    Day = Day.Fri,
    Name = "b"
};

var barComparer = new FooBarComparer();
var dayComparer = new FooDayComparer();
var compositeComparer = new CompositeFooComparer(barComparer, dayComparer);

Console.WriteLine(compositeComparer.Equals(fooA, fooB)); // displays "true"

Another idea is to have a comparer that does know which fields will be compared, based on boolean parameters instead.

public class ConfigurableFooComparer : IEqualityComparer<Foo>
{
    private readonly bool compareBar;
    private readonly bool compareName;
    private readonly bool compareDay;

    public ConfigurableFooComparer(bool compareBar, bool compareName, bool compareDay)
    {
        this.compareBar = compareBar;
        this.compareName = compareName;
        this.compareDay = compareDay;
    }

    public bool Equals(Foo x, Foo y)
    {
        if (ReferenceEquals(x, y))
        {
            return true;
        }
        if (x == null || y == null)
        {
            return false;
        }

        if (compareBar && x.Bar != y.Bar)
        {
            return false;
        }
        if (compareName && x.Name != y.Name)
        {
            return false;
        }
        if (compareDay && x.Day != y.Day)
        {
            return false;
        }

        return true;
    }

    public int GetHashCode(Foo obj)
    {
        unchecked
        {
            var hash = 0;

            if (compareBar)
            {
                hash = hash * 17 + obj.Bar.GetHashCode();
            }
            if (compareName)
            {
                hash = hash * 17 + (obj.Name == null ? 0 : obj.Name.GetHashCode());
            }
            if (compareDay)
            {
                hash = hash * 17 + obj.Day.GetHashCode();
            }

            return hash;
        }
    }

And then using it like this:

var barAndDayComparer = new ConfigurableFooComparer(compareBar: true, compareName: false, compareDay: true);
Console.WriteLine(barAndDayComparer.Equals(fooA, fooB));
Joe Sewell
  • 6,067
  • 1
  • 21
  • 34
1

You can achieve by something like this :

class Program
{
    static bool CompareFoo(Foo a, Foo b, List<IEqualityComparer<Foo>> comparers)
    {
        return comparers.All(com => com.Equals(a, b));
    }

    static void Main(string[] args)
    {
        List<IEqualityComparer<Foo>> compares = new List<IEqualityComparer<Foo>>
        {
            new FooNameComparer(),
            new FooBarComparer()
        };

        var test1 = CompareFoo(new Foo { Name = "aio", Bar = 10 }, new Foo { Name = "aio", Bar = 10 }, compares);
        var test2 = CompareFoo(new Foo { Name = "Foo1", Bar = 10 }, new Foo { Name = "Foo2", Bar = 10 }, compares);
    }
}

Note : you must consider all possible conditions in your compare classes, for example in "FooNameComparer" class the below code can become a bug :

return x.Name == y.Name;

because if the "Name" property of two classes pass null, null == null return true! the code should be :

public bool Equals(Foo x, Foo y)
{
    if (ReferenceEquals(x, y)) return true;
    if (string.IsNullOrEmpty(x?.Name) || string.IsNullOrEmpty(y?.Name))
        return false;

    return x.Name == y.Name; 
}
Mojtaba Tajik
  • 1,725
  • 16
  • 34
  • Also you can use Linq "Any()" method instead of "All()" to implement or logical condition on compares. – Mojtaba Tajik Sep 15 '18 at 04:15
  • I’m sorry, I’m not following your logic for the big. Shouldn’t `null == null` return true? – mbadawi23 Sep 15 '18 at 04:30
  • It depends on your needs, if you don’t have problem with null parameters (here Name field) your orginal code still true. I just note to you :) – Mojtaba Tajik Sep 15 '18 at 04:33
  • Also, why did you choose `List>` as the type of the parameter? I thought `param IEqualityComparer[]` might be more convenient for the user. – mbadawi23 Sep 15 '18 at 04:37
  • Two strings with value of Empty should most certainly be considered equal (Obviously it's up to the OP's use case) Properly implemented equality comparers will do a ReferenceEquals on the two arguments which will evaluate to true for nulls. You even show as much in your example. So I find the assertion that null==null should return false only if its a property of the objects in question to be highly irregular and in violation of the principle of least surprise – pinkfloydx33 Sep 15 '18 at 09:23
1

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.

Javier Capello
  • 745
  • 4
  • 13
  • Could you explain more what you mean by: "But it's cooler if they can register the FooComparer chain to the IoC Container!"? And what is "IoC"? – mbadawi23 Sep 15 '18 at 18:51
  • Also, considering your _"simplistic approach"_, Can I still have `class Foo` implement `IEquatable` and have its implementation invoked from the `GenericComparer`? – mbadawi23 Sep 16 '18 at 02:52
  • 1
    Hey there! for the first question about [IoC](https://en.wikipedia.org/wiki/Inversion_of_control) please refer to this [great answer!](https://stackoverflow.com/a/3140/2884059) An IoC container is, in very general terms, an static dictionary of dependencies that you populate at the App startup. About the second question I'm not sure of what you are trying to achieve by implementing the IEquatable interface in Foo, it kind of defeats the purpose of the GenericComparer, but it shouldn't interfere with it since the second evaluates by property and not calling Foo's Equals method. – Javier Capello Sep 16 '18 at 03:46
  • 1
    You are right it would defeat the purpose! I'm worried the future when `Foo` is extended with more fields and methods. But I guess I would use `GenericComparer` with this `FooChild` in a similar way to `Foo`. – mbadawi23 Sep 16 '18 at 04:14