目录

C#-'==','Equals()','ReferencEquals()'

核心结论

方法 默认作用
== 运算符比较
Equals() 对象逻辑相等性比较
ReferenceEquals() 比较是否为同一个对象

需要特别注意:

  • == 可以被重载(Operator Overload)
  • Equals() 可以被重写(Override)
  • ReferenceEquals() 永远比较引用,不能被重写

一、普通引用类型(Class)

定义一个普通类:

class Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }
}

创建对象:

Person p1 = new Person("Tom");
Person p2 = new Person("Tom");
Person p3 = p1;

内存关系:

p1 ──► Person("Tom")

p2 ──► Person("Tom")

p3 ──┘

其中:

  • p1 和 p2 是两个不同对象
  • p1 和 p3 指向同一个对象

示例

Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(ReferenceEquals(p1, p2));

Console.WriteLine();

Console.WriteLine(p1 == p3);
Console.WriteLine(p1.Equals(p3));
Console.WriteLine(ReferenceEquals(p1, p3));

输出:

False
False
False

True
True
True

原因

默认情况下:

==
≈ 比较引用

Equals()
≈ 比较引用

ReferenceEquals()
= 比较引用

因此三者结果完全一致。


二、重写 Equals()

很多时候我们希望:

名字相同
=
同一个人

而不是:

引用相同
=
同一个人

此时可以重写 Equals()。

class Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }

    public override bool Equals(object? obj)
    {
        return obj is Person p &&
               Name == p.Name;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

测试:

Person p1 = new Person("Tom");
Person p2 = new Person("Tom");

Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(ReferenceEquals(p1, p2));

输出:

False
True
False

原因

此时:

==
比较引用

Equals()
比较 Name

ReferenceEquals()
比较引用

三、重载 == 运算符

如果希望:

p1 == p2

也能比较内容,则需要重载运算符。

class Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }

    public override bool Equals(object? obj)
    {
        return obj is Person p &&
               Name == p.Name;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }

    public static bool operator ==(
        Person? left,
        Person? right)
    {
        if (ReferenceEquals(left, right))
            return true;

        if (left is null || right is null)
            return false;

        return left.Name == right.Name;
    }

    public static bool operator !=(
        Person? left,
        Person? right)
    {
        return !(left == right);
    }
}

测试:

Person p1 = new Person("Tom");
Person p2 = new Person("Tom");

Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(ReferenceEquals(p1, p2));

输出:

True
True
False

四、ReferenceEquals()

ReferenceEquals() 永远比较:

是否指向同一个对象

例如:

Person p1 = new Person("Tom");
Person p2 = new Person("Tom");

Console.WriteLine(
    ReferenceEquals(p1, p2)
);

输出:

False

因为:

p1 ──► 对象A

p2 ──► 对象B

即使内容完全相同,也返回 False。


五、string 的特殊情况

string s1 = new string("abc".ToCharArray());
string s2 = new string("abc".ToCharArray());

实际上:

s1 ──► 对象A

s2 ──► 对象B

是两个不同对象。

测试:

Console.WriteLine(s1 == s2);
Console.WriteLine(s1.Equals(s2));
Console.WriteLine(ReferenceEquals(s1, s2));

输出:

True
True
False

原因

string 类型已经重载了:

operator ==

因此:

==
比较字符串内容

Equals()
比较字符串内容

ReferenceEquals()
比较引用

六、低版本 C# 如何实现 Record 的值相等

Record 出现之前的标准做法

在 C# 9 之前,如果希望对象按照值进行比较,而不是按照引用进行比较,通常会实现:

IEquatable<T>
    +
Equals()
    +
GetHashCode()
    +
(可选) == 和 !=

这也是微软长期推荐的方式。


示例

public class Person : IEquatable<Person>
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public bool Equals(Person? other)
    {
        if (other is null)
            return false;

        return Name == other.Name && Age == other.Age;
    }

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

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age);
    }

    public static bool operator ==(Person? left, Person? right)
    {
        if (ReferenceEquals(left, right))
        {
            return true;
        }

        if (left is null || right is null)
        {
            return false;
        }

        return left.Equals(right);
    }

    public static bool operator !=(Person? left, Person? right)
    {
        return !(left == right);
    }
}

使用:

var p1 = new Person("Tom", 18);
var p2 = new Person("Tom", 18);

Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));

输出:

True
True

为什么推荐 IEquatable

如果只重写:

Equals(object obj)

则每次比较都需要:

object
类型检查
强制转换

而:

IEquatable<Person>

允许:

p1.Equals(p2)

直接进行强类型比较。

对于:

HashSet<T>
Dictionary<TKey,TValue>
List<T>.Contains()

等集合,性能更好。


七、Record 类型

C# 9 引入了 Record。

public record Person(string Name, int Age);

测试:

var p1 = new Person("Tom", 18);
var p2 = new Person("Tom", 18);

Console.WriteLine(p1 == p2);
Console.WriteLine(p1.Equals(p2));

输出:

True
True

Record 自动生成:

  • Equals()
  • GetHashCode()
  • == 运算符
  • != 运算符
  • ToString()

因此非常适合:

  • DTO
  • Value Object
  • 配置对象
  • 不可变对象

Record 的本质

可以理解为:

record
IEquatable<T>
+
Equals()
+
GetHashCode()
+
== 和 !=
的语法糖

编译器自动帮我们生成这些代码。


八、实际开发建议

判断内容是否相等

优先:

obj.Equals(other)

或者:

IEquatable<T>

判断是否是同一个对象

使用:

ReferenceEquals(obj1, obj2)

自定义类

如果重写了:

Equals()

必须同时重写:

GetHashCode()

否则:

HashSet<T>
Dictionary<TKey,TValue>

会出现不可预期的问题。


高频问题

==Equals() 的区别?

简答:

  • == 是运算符,可以重载
  • Equals() 是方法,可以重写
  • 默认情况下引用类型二者都比较引用
  • 对于 string、record 等类型二者通常比较内容

为什么重写 Equals() 必须重写 GetHashCode()?

因为必须保证:

a.Equals(b) == true

时:

a.GetHashCode() == b.GetHashCode()

否则哈希集合行为会异常。


Record 出现之前如何实现值相等?

标准答案:

实现 IEquatable<T>
重写 Equals()
重写 GetHashCode()
必要时重载 == 和 !=

总结

ReferenceEquals()
永远比较是否为同一个对象

Equals()
比较逻辑是否相等

==
比较规则由类型作者决定

对于普通 class:

==
≈ ReferenceEquals

Equals()
≈ ReferenceEquals

对于 string、record、自定义值对象:

==
≈ Equals()

比较的是内容

因此不要认为:

==
一定比较地址

在 C# 中,真正含义取决于该类型是否重载了 == 运算符。