Skip to content

Automatic Equals and GetHashCode of Properties

jbe2277 edited this page Jul 13, 2018 · 8 revisions

Overriding the Object.Equals and Object.GetHashCode methods to provide value equality needs a lot boilerplate code. This article shows an approach that creates this code automatically. It uses Expression Trees which allows us to create a fast implementation that comes very close to the performance of one that is implemented by user-written code.

Note: On reference types the members considered for Equals and GetHashCode must be immutable. The hash code of an object needs to be constant during the whole life-cycle.

The usage is very simple. Just inherit from Equatable<T> like the following sample code shows.

public class PersonDto : Equatable<PersonDto>
{
    private readonly string name;
    private readonly int age;

    public PersonDto(string name)
    {
        this.name = name;
    }

    public string Name { get { return name; } }

    public int Age { get { return age; } }
}

The base class just overrides the Equals and GetHashCode method and delegates the calls to the EquatableHelper static class.

public abstract class Equatable<T> : IEquatable<T> where T : Equatable<T>
{
    public override bool Equals(object other)
    {
        return Equals(other as T);
    }

    public bool Equals(T other)
    {
        return EquatableHelper.PropertiesEquals(this, other);
    }

    public override int GetHashCode()
    {
        return EquatableHelper.PropertiesGetHashCode(this);
    }

    public static bool operator ==(T x, Equatable<T> y)
    {
        return EquatableHelper.PropertiesEquals(x, y);
    }

    public static bool operator !=(T x, Equatable<T> y)
    {
        return !(x == y);
    }
}

The magic is in the EquatableHelper class. If Equals or GetHashCode is called the first time for a Type then it creates an expression that contains the implementation. During the creation of the expression we need to use reflection to get all public properties. Thus, the limitation is that just the public properties are considered.

Reflection is quite slow but fortunately the reflection code is called just once for every Type. After the expression is created we compile it into executable code. This is a great performance optimization provided by the Expression Tree functionality.

The compiled expression can be used as delegate. All further calls on the same Type are very fast because now the compiled expression does the work.

internal static class EquatableHelper
{
    private static readonly ConcurrentDictionary<Type, Func<object, object, bool>>
        getEqualsFunctions 
            = new ConcurrentDictionary<Type, Func<object, object, bool>>();
    private static readonly ConcurrentDictionary<Type, Func<object, int>> 
        getHashCodeFunctions 
            = new ConcurrentDictionary<Type, Func<object, int>>();

    public static bool PropertiesEquals(object x, object y)
    {
        if (ReferenceEquals(x, y)) { return true; }
        if (x == null || y == null) { return false; }

        Type type = x.GetType();
        var getEqualsFunction = getEqualsFunctions.GetOrAdd(type, 
                MakeEqualsMethod);
        return getEqualsFunction(x, y);
    }

    public static int PropertiesGetHashCode(object obj)
    {
        if (obj == null) { return 0; }

        Type type = obj.GetType();
        var getHashCodeFunction = getHashCodeFunctions.GetOrAdd(type, 
                MakeGetHashCodeMethod);
        return getHashCodeFunction(obj);
    }

    private static Func<object, object, bool> MakeEqualsMethod(Type type)
    {
        var paramThis = Expression.Parameter(typeof(object), "x");
        var paramThat = Expression.Parameter(typeof(object), "y");

        var paramCastThis = Expression.Convert(paramThis, type);
        var paramCastThat = Expression.Convert(paramThat, type);

        Expression last = null;
        var equalsMethod = typeof(object)
                .GetMethod("Equals", BindingFlags.Public | BindingFlags.Static);
        foreach (var property in type
                .GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            // Boxing is necessary for the call of the Equals method.
            var propertyAccessX = 
                    Expression.Convert(
                    Expression.Property(paramCastThis, property), typeof(object));
            var propertyAccessY = 
                    Expression.Convert(
                    Expression.Property(paramCastThat, property), typeof(object));

            // Use the Equals method instead of Expression.Equals 
            // which calls the "== operator"
            var equals = Expression.Call(equalsMethod, propertyAccessX, 
                    propertyAccessY);
            if (last == null)
            {
                last = equals;
            }
            else
            {
                last = Expression.AndAlso(last, equals);
            }
        }
        if (last == null)
        {
            // Type has no public instance properties: 
            // true if both types are the same
            last = Expression.Condition(Expression.TypeIs(paramThat, type), 
                    Expression.Constant(true), Expression.Constant(false));
        }
        return Expression.Lambda<Func<object, object, bool>>(last, paramThis, 
            paramThat).Compile();
    }

    private static Func<object, int> MakeGetHashCodeMethod(Type type)
    {
        ParameterExpression paramThis = Expression.Parameter(typeof(object), 
            "obj");
        UnaryExpression paramCastThis = Expression.Convert(paramThis, type);

        Expression last = null;
        Expression nullValue = Expression.Constant(null);
        foreach (PropertyInfo property in type
            .GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            // Boxing is necessary for the call of the GetHashCode method.
            var propertyAccess = Expression.Convert(
                Expression.Property(paramCastThis, property), typeof(object));
            var hash = Expression.Condition(
                Expression.Equal(propertyAccess, nullValue),
                Expression.Constant(0),
                Expression.Call(propertyAccess, "GetHashCode", Type.EmptyTypes));
            if (last == null)
            {
                last = hash;
            }
            else
            {
                last = Expression.ExclusiveOr(last, hash);
            }
        }
        if (last == null)
        {
            // Type has no public instance properties
            last = Expression.Constant(0);
        }
        return Expression.Lambda<Func<object, int>>(last, paramThis).Compile();
    }
}

Further readings

  1. Framework Design Guidelines (2nd Edition) by Krzysztof Cwalina and Brad Abrams. MSDN provides an extract of the book
  2. Member-Wise Equality Without the Boilerplate by Brad Smith
  3. EquatableHelper.cs: This code snippet provides a more complete implementation that supports collections as well.