C# 中的 ReferenceEquals 方法
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 (特殊情况)
说明:
p1
和p2
指向堆上的同一个Person
实例,所以ReferenceEquals
返回true
。p1
和p3
虽然内容相同,但分别是两个不同的对象实例,所以返回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 会将其放入“驻留池”,所有相同值的字面量都会共享同一个引用,所以 s1
和 s2
是同一个引用。但用 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最好也重写GetHashCodepublic 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); // TrueConsole.WriteLine("Equals Method:");
// Equals 方法被我们重写了,它比较的是Id字段的值
Console.WriteLine(stu1.Equals(stu2)); // True (内容相同)
Console.WriteLine(stu1.Equals(stu3)); // True (内容相同,且是同一对象)
三者的区别总结:
方法 | 比较内容 | 可被重写 | 适用于值类型 |
---|---|---|---|
ReferenceEquals | 引用地址 | 否 | 永远不适用(因装箱) |
== 运算符 | 默认是引用地址,但可重写为比较值 | 是 | 是(对于内置值类型已重写) |
Equals 实例方法 | 默认是引用地址,但通常被重写为比较值 | 是 | 是(对于内置值类型已重写) |
3. 主要使用场景
既然有 ==
和 Equals
,为什么还需要 ReferenceEquals
?
-
进行绝对的引用比较:当你明确地、故意地想知道两个变量是否指向内存中的绝对同一个实例,而不是“值”是否相等时。例如,在实现某些底层基础设施、缓存机制或监听对象身份变化的逻辑时。
-
避免被重写逻辑干扰:
==
和Equals
都可能被类重写。如果你不信任或不想依赖这些自定义的比较逻辑,ReferenceEquals
提供了一个不可被重写的、最基础的比较方式。 -
处理可能为 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
或 ==
。