HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Automatic IEqualityComparer<T>

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
automaticiequalitycomparerstackoverflow

Problem

There are APIs like the Except extension that require the IEqualityComparer to work. I find it's too much work for such a simple task to implement an interface so I thought why not automate it.

I implemented this interface in a reusable fashion so that I can use it with any value and any number of properties.

internal class AutoEqualityComparer : IEqualityComparer
{
    public AutoEqualityComparer(IEnumerable> selectors)
    {
        Selectors = selectors;
    }

    private IEnumerable> Selectors { get; }

    public bool Equals(T left, T right)
    {
        return
            !ReferenceEquals(left, null) &&
            !ReferenceEquals(right, null) &&
            Selectors.All(selector => selector(left).Equals(selector(right)));
    }

    public int GetHashCode(T obj)
    {
        unchecked
        {
            return Selectors
                .Select(selector => selector(obj).GetHashCode())
                .Aggregate(17, (hashCode, subHashCode) => hashCode * 31 + subHashCode);
        }
    }
}


I then build a new extension which also can accept any number of selectors to compare:

internal static class Enumerable
{
    public static IEnumerable Except(
        this IEnumerable first, 
        IEnumerable second, 
        params Func[] compare)
    {
        var mec = new AutoEqualityComparer(compare);
        return first.Except(second, mec);
    }
}


Example:

Select all properties of an exception that are not in the base exception:

var exceptionProperties =
    typeof(ArgumentException)
    .GetProperties()
    .Except(typeof(Exception).GetProperties(), x => x.Name);

// result: ParamName is the only property

Solution

I think if you created a projection and pass that in the class would be easier to use and more readable for anyone coming after you.

Instead of the IEnumerable> I would change it to be Func projection and lose the IEnumerable. Which would need to make the class now take two generics

public class AutoEqualityComparer : IEqualityComparer
{
    private readonly Func _projection;

    public AutoEqualityComparer(Func projection)
    {
        _projection = projection;
    }


Since we now have the strong type of class in equals we can use the EqualityComparer class for both the Equals and GetHashCode methods.

public virtual bool Equals(T x, T y)
    {
        if (x == null && y == null)
        {
            return true;
        }
        if (x == null)
        {
            return false;
        }
        if (y == null)
        {
            return false;
        }

        var xData = _projection(x);
        var yData = _projection(y);

        return EqualityComparer.Default.Equals(xData, yData);
    }

    public virtual int GetHashCode(T obj)
    {
        if (obj == null)
        {
            return 0;
        }

        var objData = _projection(obj);

        return EqualityComparer.Default.GetHashCode(objData);
    }


I would create a class to create the AutoEqualityComparer to make it easier to use

public class EqualityProjectionComparer
{
    public static AutoEqualityComparer Create(Func projection)
    {
        return new AutoEqualityComparer(projection);
    }
}


Now with the extension method you can create Tuples or anonymous classes for the IEqualityComparer

Example could be

var comparer = EqualityProjectionComparer.Create(arg => new
{
    arg.ParamName,
    arg.Message
});


Now to me this is more clear on what we are comparing and anonymous type compare the properties to see if they are equal and not reference.

Code Snippets

public class AutoEqualityComparer<T, K> : IEqualityComparer<T>
{
    private readonly Func<T, K> _projection;

    public AutoEqualityComparer(Func<T, K> projection)
    {
        _projection = projection;
    }
public virtual bool Equals(T x, T y)
    {
        if (x == null && y == null)
        {
            return true;
        }
        if (x == null)
        {
            return false;
        }
        if (y == null)
        {
            return false;
        }

        var xData = _projection(x);
        var yData = _projection(y);

        return EqualityComparer<K>.Default.Equals(xData, yData);
    }

    public virtual int GetHashCode(T obj)
    {
        if (obj == null)
        {
            return 0;
        }

        var objData = _projection(obj);

        return EqualityComparer<K>.Default.GetHashCode(objData);
    }
public class EqualityProjectionComparer<T>
{
    public static AutoEqualityComparer<T, K> Create<K>(Func<T, K> projection)
    {
        return new AutoEqualityComparer<T, K>(projection);
    }
}
var comparer = EqualityProjectionComparer<ArgumentException>.Create(arg => new
{
    arg.ParamName,
    arg.Message
});

Context

StackExchange Code Review Q#147697, answer score: 4

Revisions (0)

No revisions yet.