C# - 必须实现哪些接口 + 运算符才能获得自定义类型的值比较和相等性?

C# - What interfaces + operators must be implemented to get value comparison and equality on custom types?

假设我有一个像

这样的自定义类型
public class MyType
{
    public string name;
    public int count;

    public MyType(string n, int c)
    {
        name = n;
        count = c;
    }
}

在 C# 中,并希望对该对象的实例进行 "intuitive" 相等性比较。那是按值比较而不是参考。我知道相等性 operator == and Object.Equals() 默认引用相等性。但如果内容匹配,我希望 MyType 的两个对象相等。第一步是用类似的东西覆盖 Object.Equals()operator==

public override bool Equals(object obj)
{
   MyType t = obj as MyType;
   return (this.name == t.name) && (this.count == t.count);
}

不过,也有这样的界面:

好像用在各种比较场景中。

我是否必须实现所有这些接口以及 operator==operator!= 以确保任何涉及 MyType 的比较(包括在通用集合中的使用,例如在 List<T>.Contains()) 按值比较而不是按引用比较?或者还有其他我想念的方式吗?在我看来,七个接口和两个运算符对于实现像值比较这样简单的东西是相当多的。

您应该只需要 IEquatable<T> 以及两个运算符。至于其他的:

IEqualityComparerIEqualityComparer<T> 用于创建另一个能够确定其他对象是否相等的对象。

IComparerIComparer<T> 如果您希望能够对您的项目进行排序(因此能够确定一个是否大于另一个,不一定等同于相等)。

编辑:

另外正如 Jeff 提到的,如果您覆盖 Equals,您也应该覆盖 GetHashCode()

不需要实现这些接口就能够实现相等运算符。只需根据类型的需要覆盖它们即可。

但如果这样做,您必须覆盖Equals(object)GetHashCode()以确保正确的行为。您 应该 实施 IEquatable<T> 因为...也可以。

同样,对于比较,根据需要覆盖运算符。他们不需要接口。但是,如果你这样做,你 应该 以与操作员的行为一致的方式实施 IComparable<T>

只有在 IEqualityComparerIComparer 如果您需要更改比较对象的方式 实现后,您才真正需要实现。

The first step would be to override Object.Equals() and operator== with something like:

不,第一步是覆盖object.Equals()GetHashCode()。你必须 永远不要 重写一个而不重写另一个来对应,否则你的 class 如果用作键则有问题。

让我们看看你的Equals()

public override bool Equals(object obj)
{
   MyType t = obj as MyType;
   return (this.name == t.name) && (this.count == t.count);
}

这里有一个错误,因为如果 obj 为 null 或不是 MyType,这将抛出 NullReferenceException。让我们解决这个问题:

public override bool Equals(object obj)
{
   MyType t = obj as MyType;
   return t != null && (name == t.name) && (count == t.count);
}

我也可能将 count 比较放在 name 之前,因为如果不匹配,它可能会更快,但我不知道你的用途-case 所以可能有少量非常常见的 count 值在这种情况下不成立。不过这是一个优化问题,让我们通过给你一个相应的 GetHashCode()

来修复错误
public override int GetHashCode()
{
   return (name?.GetHashCode() ?? 0) ^ count;
}

最低要求是如果 a.Equals(b)a.GetHashCode() == b.GetHashCode() 必须为真。理想情况下,我们也希望尽可能多地散布位。我们通过将哈希码基于确定相等性的属性来实现第一(重要)部分。第二部分更复杂,但在这种情况下,字符串 GetHashCode() 的质量相对较好,这意味着仅与剩余的整数值进行异或运算可能会相当不错。搜索该站点以获取更多详细信息(包括为什么在其他情况下仅异或通常不是一个好主意)。

现在,您需要 == 语义。这是一个要求,如果你定义 == 你必须定义 !=,但我们可以很容易地根据另一个来定义一个。:

public static bool operator !=(MyType x, MyType y)
{
    return !(x == y);
}

现在,一旦我们 == 完成,!= 就会完成它。当然,我们已经定义了相等性,所以我们可以从使用它开始:

public static bool operator ==(MyType x, MyType y)
{
    return x.Equals(y);
}

这是有问题的,因为当它处理 y 为 null 时它会在 x 为 null 时抛出。我们也需要考虑这一点:

public static bool operator ==(MyType x, MyType y)
{
    if (x == null)
    {
         return y == null;
    }
    return x.Equals(y);
}

让我们考虑一下一切都必须等于它自己(事实上,如果不成立,你就会有错误)。由于我们必须考虑 x == null && y == null 的可能性,让我们将其作为 (object)x == (object)y 的示例。这让我们跳过其余的测试:

public static bool operator ==(MyType x, MyType y)
{
    if ((object)x == (object)y)
    {
        return true;
    }
    if ((object)x == null)
    {
        return false;
    }
    return x.Equals(y);
}

这有多大好处取决于与 self 进行比较的可能性有多大(作为各种事物的副作用,它可能比您想象的更常见)以及相等方法的成本有多高(在这种情况下不多,但在有更多字段进行比较的情况下,它可能相当可观)。

好的,我们已经对 EqualsGetHashCode 进行了排序,并且我们添加了 ==!=,如您所愿。拥有很好的是IEqutable<MyType>。这提供了一个强类型 Equals ,当字典、哈希集等中的比较器可用时,它将被使用。所以拥有它是件好事。这将迫使我们实现 bool Equals(MyType other) ,这与我们已经完成的覆盖非常相似,但没有转换:

public bool Equals(MyType other)
{
   return other != null && (name == other.name) && (count == other.count);
}

奖励:由于重载的工作方式,我们的 == 将调用这个稍微快一些的方法,而不是执行强制转换的覆盖。我们已经优化了 ==,并且扩展 !=,甚至没有触及它们!

现在,如果我们实施这个,那么我们必须实施GetHashCode(),这反过来意味着我们必须实施object.Equals() 覆盖,但我们已经做到了。不过我们在这里重复,所以让我们重写覆盖以使用强类型形式:

public override bool Equals(object obj)
{
  return Equals(obj as MyType);
}

全部完成。放在一起:

public class MyType : IEquatable<MyType>
{
    public string name;
    public int count;

    public MyType(string n, int c)
    {
        name = n;
        count = c;
    }

    public bool Equals(MyType other)
    {
       return other != null && (name == other.name) && (count == other.count);
    }

    public override bool Equals(object obj) => Equals(obj as MyType);

    public override int GetHashCode() => (name?.GetHashCode() ?? 0) ^ count;

    public static bool operator ==(MyType x, MyType y)
    {
        if ((object)x == (object)y)
        {
            return true;
        }

        if ((object)x == null)
        {
            return false;
        }

        return x.Equals(y);
    }

    public static bool operator !=(MyType x, MyType y) => !(x == y);
}

IComparable<T>IComparable 用于如果您还希望能够订购您的物品;说一个小于或先于另一个。平等不需要它。

IEqualityComparer<T>IEqualityComparer 用于覆盖上述所有内容,并以完全不同的方式在 another class 中定义相等性。这里的 classic 例子是有时我们希望 "abc" 等于 "ABC" 有时我们不希望,所以我们不能只依赖 ==Equals() 我们上面描述的类型的方法,因为它们只能应用一个规则。它们通常由 other classes 提供给被比较的实际 class。

假设我们有时想在比较 MyType 个实例时忽略大小写。然后我们可以这样做:

public class CaseInsensitiveMyTypeEqualityComparer : IEqualityComparer<MyType>
{
    public bool Equals(MyType x, MyType y)
    {
        if ((object)x == (object)y)
        {
            return true;
        }
        if ((object)x == null | (object)y == null)
        {
            return false;
        }
        return x.count == y.count && string.Equals(x.name, y.name, StringComparison.OrdinalIgnoreCase);
    }

    public int GetHashCode(MyType obj)
    {
        if (obj == null)
        {
            return 0;
        }
        return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.name) ^ obj.count;
    }
}

如果你用这个说:

var dictionary = new Dictionar<MyType, int>(new CaseInsensitiveMyTypeEqualityComparer());

那么字典的键将不区分大小写。请注意,由于我们根据不区分大小写的名称比较定义相等性,因此我们也必须将哈希码基于不区分大小写的哈希。

如果您不使用 IEqualityComparer<MyType>,那么字典会使用 EqualityComparer<MyType>.Default,它会使用您更高效的 IEquatable<MyType> 实现,因为它可以,并且会使用 object.Equals 如果你没有,请重写。

您可能会猜到 IEqualityComparer<T> 比仅使用 class 本身定义的相等性相对较少使用。另外,如果有人确实需要它,那个人可能不是你;它的一大优点是我们可以为其他人的 classes 定义它们。不过,这与您 class 本身的设计无关。