Pregunta

I frequently have to override Equals and GetHashCode methods for the purpose of unit testing. After this my classes begin to look like this:

public class TestItem
{
    public bool BoolValue { get; set; }

    public DateTime DateTimeValue { get; set; }

    public double DoubleValue { get; set; }

    public long LongValue { get; set; }

    public string StringValue { get; set; }

    public SomeEnumType EnumValue { get; set; }

    public decimal? NullableDecimal { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as TestItem;

        if (other == null)
        {
            return false;
        }

        if (object.ReferenceEquals(this, other))
        {
            return true;
        }

        return this.BoolValue == other.BoolValue
            && this.DateTimeValue == other.DateTimeValue
            && this.DoubleValue == other.DoubleValue // that's not a good way, but it's ok for demo
            && this.EnumValue == other.EnumValue
            && this.LongValue == other.LongValue
            && this.StringValue == other.StringValue
            && this.EnumValue == other.EnumValue
            && this.NullableDecimal == other.NullableDecimal;
    }

    public override int GetHashCode()
    {
        return this.BoolValue.GetHashCode()
            ^ this.DateTimeValue.GetHashCode()
            ^ this.DoubleValue.GetHashCode()
            ^ this.EnumValue.GetHashCode()
            ^ this.LongValue.GetHashCode()
            ^ this.NullableDecimal.GetHashCode()
            ^ (this.StringValue != null ? this.StringValue.GetHashCode() : 0);
    }
}

While it's not hard to do it, time after time it gets boring and error prone to maintain list of same fields in Equals and GetHashCode. Is there any way to list filelds used for equality checking and hash code function only once? Equals and GetHashCode should be implemented in terms of this setup list.

In my imagination configuration and usage of such setup list may look like

public class TestItem
{
    // same properties as before

    private static readonly EqualityFieldsSetup Setup = new EqualityFieldsSetup<TestItem>()
        .Add(o => o.BoolValue)
        .Add(o => o.DateTimeValue)
        // ... and so on
        // or even .Add(o => o.SomeFunction())

    public override bool Equals(object obj)
    {
        return Setup.Equals(this, obj);
    }

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

There's a way to auto implement hashCode and equals in java, project lombok for example. I wonder is there anything serving the purpose of reducing boilerplate code readily available for C#.

¿Fue útil?

Solución 2

I've done some research and found several components that were not quite what I wanted:

And also a couple of related discussions:

So far idea of having explicitly configured list of members seemed unique. And I implemented my own library https://github.com/alabax/YetAnotherEqualityComparer. It's better than the code suggested by TylerOhlsen in that it does not box extracted members and it uses EqualityComparer<T> to compare members.

Now the code looks like:

public class TestItem
{
    private static readonly MemberEqualityComparer<TestItem> Comparer = new MemberEqualityComparer<TestItem>()
        .Add(o => o.BoolValue)
        .Add(o => o.DateTimeValue)
        .Add(o => o.DoubleValue) // IEqualityComparer<double> can (and should) be specified here
        .Add(o => o.EnumValue)
        .Add(o => o.LongValue)
        .Add(o => o.StringValue)
        .Add(o => o.NullableDecimal);

    // property list is the same

    public override bool Equals(object obj)
    {
        return Comparer.Equals(this, obj);
    }

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

Also the MemberEqualityComparer implements IEqualityComparer<T> and follows its semantics: it can successfully compare default(T) which may be null for reference types and Nullables.

UPDATE: There are tools that can solve the same problem of creating member based IEqualityComparer<T> but also these can provide composite IComparer<T>!

Otros consejos

I think it would be possible to implement pretty much the same thing as Lombok in C#, but I'm not feeling that ambitious at the moment.

I believe this is what you are after though (pretty much exactly as you have described it). This implementation does box all value types into objects, so it's not the most efficient implementation, but it should be good enough for your purpose of unit tests.

public class EqualityFieldsSetup<T>
    where T : class
{
    private List<Func<T, object>> _propertySelectors;

    public EqualityFieldsSetup()
    {
        _propertySelectors = new List<Func<T, object>>();
    }

    public EqualityFieldsSetup<T> Add(Func<T, object> propertySelector)
    {
        _propertySelectors.Add(propertySelector);
        return this;
    }

    public bool Equals(T objA, object other)
    {
        //If both are null, then they are equal 
        //    (a condition I think you missed)
        if (objA == null && other == null)
            return true;

        T objB = other as T;

        if (objB == null)
            return false;

        if (object.ReferenceEquals(objA, objB))
            return true;

        foreach (Func<T, object> propertySelector in _propertySelectors)
        {
            object objAProperty = propertySelector.Invoke(objA);
            object objBProperty = propertySelector.Invoke(objB);

            //If both are null, then they are equal
            //   move on to the next property
            if (objAProperty == null && objBProperty == null)
                continue;

            //Boxing requires the use of Equals() instead of '=='
            if (objAProperty == null && objBProperty != null ||
                !objAProperty.Equals(objBProperty))
                return false;
        }

        return true;
    }

    public int GetHashCode(T obj)
    {
        int hashCode = 0;

        foreach (Func<T, object> propertySelector in _propertySelectors)
        {
            object objProperty = propertySelector.Invoke(obj);

            if (objProperty != null)
                hashCode ^= objProperty.GetHashCode();
        }

        return hashCode;
    }
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top