实现结构的价值平等

时间:2018-12-14 01:19:15

标签: c#

我读过很多东西,使结构的默认Equals()GetHashCode()方法充其量是slow,甚至在结构具有相等的不同按位表示。

因此,我编写了以下ValueEquality类:

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

    public static bool operator ==(ValueEquality<T> lhs, T rhs)
    {
        return ReferenceEquals(lhs, null) ? ReferenceEquals(rhs, null) : lhs.Equals(rhs);
    }

    public static bool operator !=(ValueEquality<T> lhs, T rhs)
    {
        return !(lhs == rhs);
    }

    public bool Equals(T other)
    {
        return ReferenceEquals(this, other) || ((!ReferenceEquals(other, null)) && EqualsNoNull(other));
    }

    public bool EqualsNoNull(T other)
    {
        IEnumerable<object> thisMembers = GetMembers();
        IEnumerable<object> otherMembers = other.GetMembers();
        return thisMembers.ObjectEquals(otherMembers);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            foreach (object member in GetMembers())
            {
                hash = hash * 31 + member.GetHashCode();
            }
            return hash;
        }
    }

    protected abstract IEnumerable<object> GetMembers();
}

如果各个枚举上的所有对应的ObjectEquals()调用都返回true(并且它们的长度相同),则IEnumerable上的Equals()扩展方法仅返回true。

这是实现此目的的示例类:

class C : ValueEquality<C>
{
    public readonly int I;
    public readonly double D;

    protected override IEnumerable<object> GetMembers()
    {
        yield return I;
        yield return D;
    }
}

这似乎效果很好。但是有两个问题:

  1. 这仅适用于类。
  2. 它仅适用于尚无子类的类。

如果我将其设置为接口,可能会失败,那么我将不得不重新实现每个方法。我想我可以将这些实现方法放在扩展方法中,但是仍然有很多样板,而现在我只需要实现一个方法即可。

所以我有两个问题:

  1. 是否有一种方法可以在不为我要实现的每个结构提供所有样板的情况下实现我正在做的事情?
  2. 如果答案为“否”(1),那么C#8中的默认接口方法是否对您有帮助?

1 个答案:

答案 0 :(得分:1)

  
      
  1. 是否有一种方法可以在不为我要实现的每个结构提供所有样板的情况下实现我正在做的事情?
  2.   

1。不是,不是。

您指出它存在一些问题,例如仅适用于课程。但是,这比这更糟,您在接下来的时间里暗示了这一点。它会将您的类型锁定为具有与其他类相同的基类,这仅仅是因为它们共享特定的实现细节。已经有一个这样的类型:System.Object。因此,在所有具有值相等的类型中,有一个子集会通过其类层次结构泄漏实现细节。这似乎是一种非常重要的代码味道。

我相信另一个缺点是-而且我对此不是专家-哈希码计算并非千篇一律。如果我是对的,那么您将需要能够为每个类选择正确的组合算法。

  
      
  1. 如果答案为“否”(1),那么C#8中的默认接口方法是否对您有帮助?
  2.   

2。绝对不会。

一方面,为了提供接口方法的默认实现,您必须控制该接口。由于您既无法控制Object(也无法控制接口),也无法控制IEquatable<T>,因此无法为其方法创建默认实现。

此外,Object已具有默认实现。这就是您要避免的事情。 IEquatable<T> 可以由BCL根据Object.Equals的默认实现,但这倒退了,这也是您要避免的事情。副作用包括值类型的装箱,即使没有被覆盖也调用Equals(Object)以及开发人员轻度迷惑。

最后,引入新接口不会产生任何效果,因为1)您能够提供的默认实现将是针对该接口中的方法,而不是ObjectIEquatable<T>中的方法,以及2)框架中的任何内容都不会有意义地意识到新接口,因此它将永远不会被有意义地调用(即用于相等性测试)。

去哪里

对于您尝试做的事情可能看起来不太好,但我不想空手而归。

如果您想要可重用的实现,建议创建一个实用程序类,该类提供作为静态方法的功能。它确实在每种类型中都涉及一些样板代码,但是它不仅比将其写出来更为统一,而且可以使用代码生成或代码片段(使我更倾向于后者,因为我自己很简单)而使编写代码的工作量最小化。

因为它不需要继承,所以您也可以将其用于值类型,并且代码几乎相同。

// Signatures for up to 4 fields.

public static class EqualityUtility
{
    public static int GetHashCode<T1>(T prop1);
    public static int GetHashCode<T1, T2>(T prop1, T2 prop2);
    public static int GetHashCode<T1, T2, T3>(T prop1, T2 prop2, T3 prop3);
    public static int GetHashCode<T1, T2, T3, T4>(T prop1, T2 prop2, T3 prop3, T4 prop4);

    public static bool OtherIsNotNull<T>(T other) where T : class => !(other is null);

    public static bool Equals<T1>(T1 obj1prop1, T1 obj2prop1);
    public static bool Equals<T1, T2>(T1 obj1prop1, T1 obj2prop1, T2 obj1prop2, T2 obj2prop2);
    public static bool Equals<T1, T2, T3>(T1 obj1prop1, T1 obj2prop1, T2 obj1prop2, T2 obj2prop2, T3 obj1prop3, T3 obj2prop3);
    public static bool Equals<T1, T2, T3, T4>(T1 obj1prop1, T1 obj2prop1, T2 obj1prop2, T2 obj2prop2, T3 obj1prop3, T3 obj2prop3, T4 obj1prop4, T4 obj2prop4);
}

不,不是很漂亮。是的,它很快变得笨拙。

// Demonstrations with 3 fields:

public struct TestStruct : IEquatable<TestStruct>
{
    private int _field1;
    private string _field2;
    private double _field3;

    // Concerns like constructors excluded for brevity...

    public override int GetHashCode() => EqualityUtility.GetHashCode(_field1, _field2, _field3);

    public override bool Equals(object obj) => obj is TestStruct other && Equals(other);

    public bool Equals(TestStruct other) =>
        EqualityUtility.Equals(_field1, other.field1, _field2, other.field2, _field3, other._field3);

    // Everything else would be implemented in terms of what you already have.
    public static bool operator ==(TestStruct a, TestStruct b) => a.Equals(b);
    public static bool operator !=(TestStruct a, TestStruct b) => !a.Equals(b);
}

public class TestClass : IEquatable<TestClass>
{
    private int _field1;
    private string _field2;
    private double _field3;

    // Concerns like constructors excluded for brevity...

    public override int GetHashCode() => EqualityUtility.GetHashCode(_field1, _field2, _field3);

    public override bool Equals(object obj) => obj is TestClass other && Equals(other);

    public bool Equals(TestClass other) =>
        EqualityUtility.OtherIsNotNull(other) &&
        EqualityUtility.Equals(_field1, other.field1, _field2, other.field2, _field3, other._field3);

    public static bool operator ==(TestClass a, TestClass b) => (a is null && b is null) || (!(a is null) && a.Equals(b));
    public static bool operator !=(TestClass a, TestClass b) => !(a == b);
}

该实用工具类可以作为(私有?)NuGet软件包提供,其中包含被引用的程序集或随项目进行编译的代码文件(在这种情况下,应将类设置为内部)。