相等

相等性 属于 C#

5 个练习

关于 相等性

这个编程练习展示了 C# 中相等性的若干性质:

Object.Equals()

编程练习涵盖的主题

  • 简单类型(字符串和基元类型)通常用==和!=测试相等性。这被认为比使用这些类型同样提供的 Equals() 方法更符合惯用法。Java 程序员在处理字符串时应当留意:==在 C# 中按值比较,而回到 Java 后按引用比较。
  • 引用类型(类的实例)使用从object继承来的Equals()方法进行比较。如果你做相等性测试的目的是确认两个对象是同一个实例,那么依赖object的实现就足够了。如果不是,就需要重写object.Equals()。
  • 如果你知道自己类的所有实例都在同一个地方创建,比如某个游戏或模拟程序中的角色,那么引用相等性就足够了。不过,同一个现实世界实体很可能会被创建出多个实例(例如来自数据库、来自用户输入、通过网络请求)。这时就必须对能唯一标识该实体的值做相等性测试。因此必须重写Equals()。
  • 重写后的Equals()会对简单类型的成员用==做相等性测试,对引用类型的成员则递归调用Equals()。
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • 静态方法object.ReferenceEquals()用于比较两个对象,判断它们是不是同一个实例。这样写更清晰,而且在Equals()和==已被重写或重载的情况下也是必需的。
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

其他相关话题

  • 除了public override bool Equals(object obj)之外,IDE 通常还会生成重载protected bool Equals(FacialFeatures other),供涉及继承时使用。派生类可以调用基类的Equals(),然后再加上自己的测试。
  • 除非你在自己的类里重载了==运算符以及Equals()方法(参见operator-overloading练习),或者你只关心引用是否相等,否则不要使用==。
  • structs中的相等性测试在structs练习中讨论。
  • tuples的相等性在tuples练习中讨论。
  • 很多开发者依赖 IDE 来生成相等性方法的实现,因为 IDE 会处理相等性方面的所有细枝末节。例如,JetBrains 的 RIDER(v 2020.1)会为一个类生成下面这些相等性方法:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((T) obj);
}
  • 如果你打算改进 IDE 生成的代码,要小心。这些代码通常能稳妥地应对 null 和类型错误的对象,而且优化得相当不错。
  • 委托的相等性测试在练习中没有专门讨论。
  • 数组和大多数集合都没有内置的相等性测试。LINQ(在后面的练习中会讨论)提供了SequenceEquals(),但没有 LINQ 时,就只能遍历两个集合并逐个比较元素。
  • 关于如何在自己的类中使用==和!=,参见operator-overloading练习。

Object.GetHashCode()

  • object.GetHashCode()返回一个 32 位整数形式的哈希码。字典和集合类(比如Dictionary<T>和HashSet<T>)用哈希码来高效地存储和检索对象。对于字典来说,哈希针对的是键。
  • C# 开发者中有一个共识:如果你重写了Equals(),也应该重写GetHashCode()。Equals()和GetHashCode()之间存在一种关系,字典、哈希集以及任何使用哈希码的类要想行为正确,这种关系就必须成立。你应该正确地实现这个方法,以免给日后可能添加基于哈希码的集合的维护者埋下陷阱。
  • 哈希码与相等性之间的关系是:如果两个对象相等(Equal()返回 true),那么这两个对象的GetHashCode()必须返回相同的值。反过来则不成立。这种关系并不对称。想象一个查找函数:它先根据哈希码找到一个“桶”,然后用相等性测试挑出具体的项。
  • 创建哈希码最简单的方法是调用HashCode.Combine(),传入相等性测试中用到的值(或其中一部分)。请记住,传给Combine()的信息越多,哈希实现很可能就越高效。
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • 在使用哈希集合期间,相等性测试中用到的值必须保持稳定。如果你用一组值把对象加入集合,然后又修改了这些值,哈希码就不再指向正确的“桶”。实际上这意味着对象应该是不可变的。其他做法则有可能给维护者带来麻烦。不可变性在其他练习中讨论。
  • 你有可能设计出比库例程生成的更好的哈希码,但这要么是因为你对数据的特征有深入的了解,要么是因为集合非常简单,值可以不经哈希直接使用。多花这份力气也许并不值得。

性能优化

要想略微提升性能,尤其是在对象属于集合的情况下,可以添加一个重载成员public bool Equals(T other)。

这样对引用类型可以省去一定量的类型检查,对值类型则可以省去一次装箱,因为值类型不必再被转换成对象(装箱)作为public override bool Equals(object other)的实参。

如果你的类添加了IEquatable<T>接口,就必须实现这个重载。除非你的代码里有接收IEquatable<T>类型对象的例程(这些例程大概会做一些与相等性有关的有意思的事情,而且不关心实现类是什么),否则其实没有别的令人信服的理由要包含这个接口。

IEqualityComparer<T>

如果一个类可以用两种不同的方式唯一标识,比如一个Person类既有 SSID 又有唯一的电子邮箱地址,那么 .NET 提供了一种办法,让两个不同的集合使用不同的哈希码和相等性测试。每个集合都可以有IEqualityComparer<T>的不同实现,各自带有自己的Equals()和GetHashCode()方法。你可以有一个以 SSID 为键的字典,另一个以电子邮箱地址为键的字典。

在使用IEqualityComparer<T>时,通常仍要在你的条目类上重写Equals()和GetHashCode(),以避免集合类之外出现问题。

使用IEqualityComparer<T>时需要考虑的一点是,私有方法等不会对相等性测试可用。

如果只涉及一个哈希集合,那么最好避免使用IEqualityComparer<T>,而把相等性和哈希码封装在对象自身之中。对象与哈希集合之间的这种关键依赖无法由编译器强制保证,这本身就不理想;但如果把哈希和比较逻辑放到别处,维护者就更容易在修改类的成员时不顾及对集合的影响。

关于浮点数相等性的说明

对粗心的程序员来说,一个可能带来挑战的基元类型就是测试浮点数值的相等性。这一点在floating-point-numbers概念的 about.md 文档中讨论。

相等性与继承

这篇文章展示了在相等性和继承方面必须做出的一些决定。只有当你同时操作某个类及其派生类的实例、并且需要测试两者之间的相等性时,这些问题才会与你有关。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 相等性

练习已锁定

再解锁 4 个练习即可练习 相等性