【问题标题】:Is there a way to reduce amount of boilerplate code in Equals and GetHashCode?有没有办法减少 Equals 和 GetHashCode 中的样板代码量?
【发布时间】:2013-07-07 15:28:16
【问题描述】:

出于单元测试的目的,我经常不得不重写 EqualsGetHashCode 方法。在此之后,我的课程开始看起来像这样:

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);
    }
}

虽然做起来并不难,但在EqualsGetHashCode 中维护相同字段的列表时会变得无聊且容易出错。有没有办法只列出一次用于相等检查和哈希码功能的文件? Equals 和 GetHashCode 应该按照这个设置列表来实现。

在我的想象中,此类设置列表的配置和使用可能看起来像

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);
    }
}

有一种方法可以在 java 中自动实现 hashCodeequals,例如 project lombok。我想知道是否有任何东西可以减少 C# 的样板代码。

【问题讨论】:

  • 使用对象状态的正确表示覆盖 tostring,其哈希码将基于该输入。
  • @terrybozzio ToString() 比较?永远不要那样做。首先,它很昂贵。比较函数应该很快。其次,它甚至没有削减样板,它只是将它移动到其他地方。
  • @Mike:我对此表示怀疑。您上面的 GetHashCode 实现非常具体。我可能对那个实现根本不满意。与 Equals 相同(在较小程度上)。
  • 使用正确的 tostring 覆盖是 testitem 类等于覆盖将减少以返回 obj.ToString() == this.ToString()。由于一切都支持 ToString 我们甚至不必检查正确的类型
  • @terrybozzio,但您仍然必须实现同样繁琐的 .ToString() 方法。

标签: c# equals gethashcode


【解决方案1】:

我做了一些研究,发现几个组件不是我想要的:

还有一些相关的讨论:

到目前为止,明确配置成员列表的想法似乎是独一无二的。我实现了我自己的库https://github.com/alabax/YetAnotherEqualityComparer。它比 TylerOhlsen 建议的代码更好,因为它不会对提取的成员进行装箱,而是使用 EqualityComparer&lt;T&gt; 来比较成员。

现在代码如下:

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);
    }
}

MemberEqualityComparer 还实现了IEqualityComparer&lt;T&gt; 并遵循其语义:它可以成功地比较default(T) 这可能是null 的引用类型和Nullables。

更新:工具可以解决基于IEqualityComparer&lt;T&gt;创建成员的相同问题,但这些工具也可以提供复合IComparer&lt;T&gt;

【讨论】:

    【解决方案2】:

    我认为在 C# 中实现与 Lombok 几乎相同的东西是可能的,但我目前并没有那么雄心勃勃。

    我相信这就是您所追求的(与您所描述的几乎完全一样)。此实现确实将所有值类型装箱到对象中,因此它不是最有效的实现,但它应该足以满足您的单元测试目的。

    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;
        }
    }
    

    【讨论】:

    • 太好了,构建这样的工具看起来并不难!这里的问题是您必须将 lambda 返回值转换为object。我建议使用EquailtyComparer&lt;T&gt; 来比较成员值。
    • 即使属性存储在对象类型的变量中,Equals 方法仍将使用派生类型的(可能被覆盖的)Equals 方法。我在这里写的在功能上等同于你上面的。唯一的区别是值类型被隐式地装箱到对象中。
    • 在你的 lambda 中转换为对象可以是隐式的。从语法上讲,这个答案与您在问题中所建议的完全一样。从功能上讲,它是等价的(需要对值类型进行框选)。
    猜你喜欢
    • 2011-03-03
    • 1970-01-01
    • 1970-01-01
    • 2018-03-03
    • 2019-02-16
    • 1970-01-01
    • 1970-01-01
    • 2012-06-18
    • 2011-11-23
    相关资源
    最近更新 更多