C# 中的 ReferenceEquals 方法

1. 核心定义与作用

Object.ReferenceEquals 是一个静态方法,它的作用非常纯粹和单一:

判断两个对象引用是否指向内存中的同一个实例(即同一个对象)。

  • 返回值bool 类型。如果两个引用指向同一个对象,返回 true;否则返回 false
  • 它不关心:对象的内容是什么、对象的类型是否相同、== 运算符或 Equals 方法是如何被重写的。它只进行引用同一性的比较。

它的方法签名如下:

public static bool ReferenceEquals (object? objA, object? objB);

2. 工作原理与举例说明

让我们通过一系列例子来彻底理解它。

例 1:引用类型的基本行为
class Person
{
public string Name {
get;
set;
}
}
Person p1 = new Person { Name = "Alice"
};
Person p2 = p1;
// p2 是 p1 的引用副本,它们指向同一个对象
Person p3 = new Person { Name = "Alice"
};
// 新对象,内容虽然和 p1 一样,但内存地址不同
Console.WriteLine(Object.ReferenceEquals(p1, p2));
// 输出: True
Console.WriteLine(Object.ReferenceEquals(p1, p3));
// 输出: False
Console.WriteLine(Object.ReferenceEquals(null, null));
// 输出: True (特殊情况)

说明

  • p1p2 指向堆上的同一个 Person 实例,所以 ReferenceEquals 返回 true
  • p1p3 虽然内容相同,但分别是两个不同的对象实例,所以返回 false
  • 两个 null 引用被认为是相等的。
例 2:字符串的特殊情况 - 字符串驻留

字符串 (string) 在 C# 中是不可变的引用类型,但 CLR 使用了一种叫“字符串驻留”的优化技术,这会让 ReferenceEquals 的行为变得有趣。

string s1 = "Hello";
string s2 = "Hello";
// 编译器会进行驻留,s2 和 s1 指向同一个内存地址
string s3 = new string("Hello".ToCharArray());
// 强制在堆上创建一个新的字符串对象
Console.WriteLine(Object.ReferenceEquals(s1, s2));
// 输出: True (因为驻留)
Console.WriteLine(Object.ReferenceEquals(s1, s3));
// 输出: False (不同对象)
// 使用String.Intern方法将s3驻留,之后获取的引用就是驻留池中的引用
string s4 = String.Intern(s3);
Console.WriteLine(Object.ReferenceEquals(s1, s4));
// 输出: True

说明:对于字面量字符串,CLR 会将其放入“驻留池”,所有相同值的字面量都会共享同一个引用,所以 s1s2 是同一个引用。但用 new 等方式创建的字符串对象不会自动驻留。

例 3:值类型的比较 - 装箱

ReferenceEquals 的参数是 object,所以当传递值类型(如 int, struct)时,会发生装箱

int num1 = 10;
int num2 = 10;
// 值类型传递给ReferenceEquals时会被装箱
// num1被装箱到一个新的object实例中
// num2被装箱到另一个新的object实例中
// 两个不同的装箱对象,引用自然不同
Console.WriteLine(Object.ReferenceEquals(num1, num2));
// 输出: False
// 更明显的例子:和自己比较
Console.WriteLine(Object.ReferenceEquals(num1, num1));
// 输出: False 

这是最重要的陷阱!
Object.ReferenceEquals(num1, num1) 也返回 false,因为每次装箱都会产生一个新的临时对象。所以,ReferenceEquals 方法永远不适用于比较值类型,它的结果总是 false(除非比较 null)。

例 4:与 ==Equals 的对比
class Student
{
public string Id {
get;
set;
}
// 假设我们重写了Equals,只比较Id字段
public override bool Equals(object obj) => obj is Student s && Id == s.Id;
// 重写Equals最好也重写GetHashCode
public override int GetHashCode() => Id?.GetHashCode() ?? 0;
}
Student stu1 = new Student { Id = "001"
};
Student stu2 = new Student { Id = "001"
};
Student stu3 = stu1;
Console.WriteLine("ReferenceEquals:");
Console.WriteLine(Object.ReferenceEquals(stu1, stu2));
// False (不同对象)
Console.WriteLine(Object.ReferenceEquals(stu1, stu3));
// True (同一对象)
Console.WriteLine("== Operator:");
// == 默认行为与ReferenceEquals相同,除非被重写
// 假设我们没有重写 == 运算符,所以它执行引用比较
Console.WriteLine(stu1 == stu2);
// False
Console.WriteLine(stu1 == stu3);
// True
Console.WriteLine("Equals Method:");
// Equals 方法被我们重写了,它比较的是Id字段的值
Console.WriteLine(stu1.Equals(stu2));
// True (内容相同)
Console.WriteLine(stu1.Equals(stu3));
// True (内容相同,且是同一对象)

三者的区别总结

方法比较内容可被重写适用于值类型
ReferenceEquals引用地址永远不适用(因装箱)
== 运算符默认是引用地址,但可重写为比较值是(对于内置值类型已重写)
Equals 实例方法默认是引用地址,但通常被重写为比较值是(对于内置值类型已重写)

3. 主要使用场景

既然有 ==Equals,为什么还需要 ReferenceEquals

  1. 进行绝对的引用比较:当你明确地、故意地想知道两个变量是否指向内存中的绝对同一个实例,而不是“值”是否相等时。例如,在实现某些底层基础设施、缓存机制或监听对象身份变化的逻辑时。

  2. 避免被重写逻辑干扰==Equals 都可能被类重写。如果你不信任或不想依赖这些自定义的比较逻辑,ReferenceEquals 提供了一个不可被重写的、最基础的比较方式。

  3. 处理可能为 null 的对象:它是静态方法,即使参数为 null 也不会抛出异常,比直接使用 == 在某些复杂情况下更安全。

// 一个实用的例子:在实现Equals时,先进行引用比较以优化性能
public override bool Equals(object obj)
{
// 如果引用相同,肯定是同一个对象,无需继续比较字段
if (Object.ReferenceEquals(this, obj))
return true;
// 如果对方为null或类型不同,肯定不相等
if (obj is null || this.GetType() != obj.GetType())
return false;
// 最后再进行耗时的字段逐一比较
// ... 比较各个字段的值
}

总结

  • Object.ReferenceEquals 只检查引用是否相同,不检查值。
  • 永远不会用于值类型,因为装箱会产生临时对象,导致比较结果总是 false
  • 对于字符串,要小心字符串驻留带来的影响。
  • 它的主要用途是进行身份识别(Identity Check)而不是值相等性检查,常用于底层实现或需要绕过自定义相等性逻辑的场景。

简单来说,当你问“是同一个东西吗?”时,用 ReferenceEquals;当你问“看起来一样吗?”时,用 Equals==

posted on 2025-09-23 08:16  lxjshuju  阅读(9)  评论(0)    收藏  举报