C#中的.Equals()方法,.ReferenceEquals()方法和==运算符

102

对这三个方法我的理解如下:

  • .Equals() 方法测试数据相等性(用缺乏更好的描述来表达)。.Equals() 可以为同一个对象的不同实例返回True,并且这是最常被重写的方法。

  • .ReferenceEquals() 方法测试两个对象是否是同一实例,该方法不能被重写。

  • == 运算符默认情况下等同于 ReferenceEquals() 方法,但是它可以被重写。

然而C#站点则指出:

在对象类中,EqualsReferenceEquals 方法在语义上是等效的,唯一的区别是 ReferenceEquals 仅适用于对象实例。 ReferenceEquals 方法是静态的。

现在我有些糊涂了。请问有人能够解释一下吗?


请参考https://dev59.com/23RA5IYBdhLWcg3wzhbZ以及其他关于此主题的StackOverflow问题。 - Ian Mercer
@High,我有问题。我只是从C# Station中提取的部分让我感到困惑。 - 999999
7个回答

106
你感到困惑的原因似乎是C#站点提取的部分存在错别字,应该写成:"...除了Equals只适用于对象实例。ReferenceEquals方法是静态的。"
你对每个操作的语义意义差异和可以被覆盖的范围都有一定的理解(虽然"相同对象的不同实例"似乎有点混淆,应该改为"相同类型的不同实例"),现在我们来看看问题的最后一部分,也就是它们如何与普通的System.Object实例和System.Object引用一起使用(我们需要两者来避免==的非多态性)。在这里,所有三种操作将以等效方式工作,但是有一个注意点:Equals不能在null上调用。 Equals是一个实例方法,接受一个参数(可以是null)。由于它是一个实例方法(必须在实际对象上调用),因此不能在null引用上调用。 ReferenceEquals是一个静态方法,接受两个参数,其中一个/两个可以是null。由于它是静态的(不与对象实例相关),因此不会在任何情况下抛出NullReferenceException==是一个运算符,在这种情况下(object),其行为与ReferenceEquals完全相同。它也不会抛出NullReferenceException
举个例子:
object o1 = null;
object o2 = new object();

//Technically, these should read object.ReferenceEquals for clarity, but this is redundant.
ReferenceEquals(o1, o1); //true
ReferenceEquals(o1, o2); //false
ReferenceEquals(o2, o1); //false
ReferenceEquals(o2, o2); //true

o1.Equals(o1); //NullReferenceException
o1.Equals(o2); //NullReferenceException
o2.Equals(o1); //false
o2.Equals(o2); //true

1
摘录中提到了“在object类中”。我觉得你可能忽略了这一部分?否则的话,你不会谈论覆盖它。 - Domenic
2
我的回答仅限于object类。 - Ani
2
“Equals”也是object上的一个静态方法,它接受两个参数。其中一个或两个参数可以为null - weston
@weston 这不正确,Equals 只接受一个参数,另一个值被视为调用它的对象 this。如果你调用 obj.Equals(obj2),当 obj == null 时,你将会得到一个异常,因为 .Equals 是 Object 类的一个方法,不能在 null 上调用。 - Phoera
1
@Phoera,我现在明白你所指的了,但正如我所说,它也是一个带有两个参数的静态方法:https://learn.microsoft.com/en-us/dotnet/api/system.object.equals?view=net-5.0#System_Object_Equals_System_Object_System_Object_ 例如 Object.Equals(a, b) - weston
显示剩余6条评论

27
请查看这篇关于该主题的MSDN文章
我认为相关要点如下:

如果要检查引用相等,请使用ReferenceEquals。如果要检查值相等,请使用Equals或Equals。

默认情况下,操作符==通过确定两个引用是否指示同一对象来测试引用相等性,因此引用类型不需要实现操作符==以获得此功能。当一个类型是不可变的,意味着实例中包含的数据无法更改时,重载操作符==以比较值相等性而不是引用相等性可以很有用,因为作为不可变对象,只要它们具有相同的值,它们就可以被视为相同。

希望这能帮助你!

13
很遗憾,链接已经失效。因为复制相关信息而给你加1分。 - Pac0

10

您对.ReferenceEquals的理解是正确的。

.Equals用于检查值类型的数据相等性,并用于非值类型(一般对象)的引用相等性。

可以重写.Equals方法以对对象执行某种形式的数据相等性检查。

编辑:此外,.ReferenceEquals不能用于值类型(虽然可以使用,但始终为false)


3
我想补充一点有关与 "null" 比较的内容。
  1. ReferenceEquals(object, object) 相当于 "(object)arg1 == arg2"(所以对于值类型,需要装箱并且需要时间)。但是在几种情况下,这个方法是唯一百分之百安全的方式来检查参数是否为 null,例如:

    • a)在通过 . 运算符调用其成员之前
    • b)检查 AS 运算符的结果。
  2. == 和 Equals()。为什么我说 ReferenceEquals 在进行空检查时是 100% 安全的呢?想象一下,当您在核心跨项目库中编写通用扩展时,您可以限制通用参数类型为某个域类型。这个类型可能会引入“==”运算符--现在或以后(相信我,我已经看过很多了,特别是当涉及到域或持久化对象时,这个运算符可能具有非常“奇怪”的逻辑)。您尝试检查参数是否为空,然后调用成员操作。令人惊讶的是,这里可能会出现 NullRef。因为“==”运算符与 Equals() 几乎相同--非常定制化和非常不可预测。但是有一个区别应该考虑--如果您没有将通用参数限制为某个自定义类型(只有当您的类型为“类”时才能使用 == 运算符),则“==”运算符与 object.ReferenceEquals(..) 相同。Equals 实现总是从最终类型中使用,因为它是虚拟的。

因此,我的建议是,在编写自己的类型或从知名类型派生时,可以使用 == 检查 null。否则,使用 object.ReferenceEquals(arg, null)。


2
我扩展了Ani的出色回答,以展示处理引用类型和重写相等方法时的关键差异。

.

void Main()
{

    //odd os are null; evens are not null
    object o1 = null;
    object o2 = new object();
    object o3 = null;
    object o4 = new object();
    object o5 = o1;
    object o6 = o2;

    Demo d1 = new Demo(Guid.Empty);
    Demo d2 = new Demo(Guid.NewGuid());
    Demo d3 = new Demo(Guid.Empty);

    Debug.WriteLine("comparing null with null always yields true...");
    ShowResult("ReferenceEquals(o1, o1)", () => ReferenceEquals(o1, o1)); //true
    ShowResult("ReferenceEquals(o3, o1)", () => ReferenceEquals(o3, o1)); //true
    ShowResult("ReferenceEquals(o5, o1)", () => ReferenceEquals(o5, o1)); //true 
    ShowResult("o1 == o1", () => o1 == o1); //true
    ShowResult("o3 == o1", () => o3 == o1); //true
    ShowResult("o5 == o1", () => o5 == o1); //true 

    Debug.WriteLine("...though because the object's null, we can't call methods on the object (i.e. we'd get a null reference exception).");
    ShowResult("o1.Equals(o1)", () => o1.Equals(o1)); //NullReferenceException
    ShowResult("o1.Equals(o2)", () => o1.Equals(o2)); //NullReferenceException
    ShowResult("o3.Equals(o1)", () => o3.Equals(o1)); //NullReferenceException
    ShowResult("o3.Equals(o2)", () => o3.Equals(o2)); //NullReferenceException
    ShowResult("o5.Equals(o1)", () => o5.Equals(o1));  //NullReferenceException
    ShowResult("o5.Equals(o2)", () => o5.Equals(o1));  //NullReferenceException

    Debug.WriteLine("Comparing a null object with a non null object always yeilds false");
    ShowResult("ReferenceEquals(o1, o2)", () => ReferenceEquals(o1, o2)); //false
    ShowResult("ReferenceEquals(o2, o1)", () => ReferenceEquals(o2, o1)); //false
    ShowResult("ReferenceEquals(o3, o2)", () => ReferenceEquals(o3, o2)); //false
    ShowResult("ReferenceEquals(o4, o1)", () => ReferenceEquals(o4, o1)); //false
    ShowResult("ReferenceEquals(o5, o2)", () => ReferenceEquals(o3, o2)); //false
    ShowResult("ReferenceEquals(o6, o1)", () => ReferenceEquals(o4, o1)); //false
    ShowResult("o1 == o2)", () => o1 == o2); //false
    ShowResult("o2 == o1)", () => o2 == o1); //false
    ShowResult("o3 == o2)", () => o3 == o2); //false
    ShowResult("o4 == o1)", () => o4 == o1); //false
    ShowResult("o5 == o2)", () => o3 == o2); //false
    ShowResult("o6 == o1)", () => o4 == o1); //false
    ShowResult("o2.Equals(o1)", () => o2.Equals(o1)); //false
    ShowResult("o4.Equals(o1)", () => o4.Equals(o1)); //false
    ShowResult("o6.Equals(o1)", () => o4.Equals(o1)); //false

    Debug.WriteLine("(though again, we can't call methods on a null object:");
    ShowResult("o1.Equals(o2)", () => o1.Equals(o2)); //NullReferenceException
    ShowResult("o1.Equals(o4)", () => o1.Equals(o4)); //NullReferenceException
    ShowResult("o1.Equals(o6)", () => o1.Equals(o6)); //NullReferenceException

    Debug.WriteLine("Comparing 2 references to the same object always yields true");
    ShowResult("ReferenceEquals(o2, o2)", () => ReferenceEquals(o2, o2)); //true    
    ShowResult("ReferenceEquals(o6, o2)", () => ReferenceEquals(o6, o2)); //true <-- Interesting
    ShowResult("o2 == o2", () => o2 == o2); //true  
    ShowResult("o6 == o2", () => o6 == o2); //true <-- Interesting
    ShowResult("o2.Equals(o2)", () => o2.Equals(o2)); //true 
    ShowResult("o6.Equals(o2)", () => o6.Equals(o2)); //true <-- Interesting

    Debug.WriteLine("However, comparing 2 objects may yield false even if those objects have the same values, if those objects reside in different address spaces (i.e. they're references to different objects, even if the values are similar)");
    Debug.WriteLine("NB: This is an important difference between Reference Types and Value Types.");
    ShowResult("ReferenceEquals(o4, o2)", () => ReferenceEquals(o4, o2)); //false <-- Interesting
    ShowResult("o4 == o2", () => o4 == o2); //false <-- Interesting
    ShowResult("o4.Equals(o2)", () => o4.Equals(o2)); //false <-- Interesting

    Debug.WriteLine("We can override the object's equality operator though, in which case we define what's considered equal");
    Debug.WriteLine("e.g. these objects have different ids, so we treat as not equal");
    ShowResult("ReferenceEquals(d1,d2)",()=>ReferenceEquals(d1,d2)); //false
    ShowResult("ReferenceEquals(d2,d1)",()=>ReferenceEquals(d2,d1)); //false
    ShowResult("d1 == d2",()=>d1 == d2); //false
    ShowResult("d2 == d1",()=>d2 == d1); //false
    ShowResult("d1.Equals(d2)",()=>d1.Equals(d2)); //false
    ShowResult("d2.Equals(d1)",()=>d2.Equals(d1)); //false
    Debug.WriteLine("...whilst these are different objects with the same id; so we treat as equal when using the overridden Equals method...");
    ShowResult("d1.Equals(d3)",()=>d1.Equals(d3)); //true <-- Interesting (sort of; different to what we saw in comparing o2 with o6; but is just running the code we wrote as we'd expect)
    ShowResult("d3.Equals(d1)",()=>d3.Equals(d1)); //true <-- Interesting (sort of; different to what we saw in comparing o2 with o6; but is just running the code we wrote as we'd expect)
    Debug.WriteLine("...but as different when using the other equality tests.");
    ShowResult("ReferenceEquals(d1,d3)",()=>ReferenceEquals(d1,d3)); //false <-- Interesting (sort of; same result we had comparing o2 with o6; but shows that ReferenceEquals does not use the overridden Equals method)
    ShowResult("ReferenceEquals(d3,d1)",()=>ReferenceEquals(d3,d1)); //false <-- Interesting (sort of; same result we had comparing o2 with o6; but shows that ReferenceEquals does not use the overridden Equals method)
    ShowResult("d1 == d3",()=>d1 == d3); //false <-- Interesting (sort of; same result we had comparing o2 with o6; but shows that ReferenceEquals does not use the overridden Equals method)
    ShowResult("d3 == d1",()=>d3 == d1); //false <-- Interesting (sort of; same result we had comparing o2 with o6; but shows that ReferenceEquals does not use the overridden Equals method)


    Debug.WriteLine("For completeness, here's an example of overriding the == operator (wihtout overriding the Equals method; though in reality if overriding == you'd probably want to override Equals too).");
    Demo2 d2a = new Demo2(Guid.Empty);
    Demo2 d2b = new Demo2(Guid.NewGuid());
    Demo2 d2c = new Demo2(Guid.Empty);
    ShowResult("d2a == d2a", () => d2a == d2a); //true
    ShowResult("d2b == d2a", () => d2b == d2a); //false
    ShowResult("d2c == d2a", () => d2c == d2a); //true <-- interesting
    ShowResult("d2a != d2a", () => d2a != d2a); //false
    ShowResult("d2b != d2a", () => d2b != d2a); //true
    ShowResult("d2c != d2a", () => d2c != d2a); //false <-- interesting
    ShowResult("ReferenceEquals(d2a,d2a)", () => ReferenceEquals(d2a, d2a)); //true
    ShowResult("ReferenceEquals(d2b,d2a)", () => ReferenceEquals(d2b, d2a)); //false
    ShowResult("ReferenceEquals(d2c,d2a)", () => ReferenceEquals(d2c, d2a)); //false <-- interesting
    ShowResult("d2a.Equals(d2a)", () => d2a.Equals(d2a)); //true
    ShowResult("d2b.Equals(d2a)", () => d2b.Equals(d2a)); //false
    ShowResult("d2c.Equals(d2a)", () => d2c.Equals(d2a)); //false <-- interesting   

}



//this code's just used to help show the output in a friendly manner
public delegate bool Statement();
void ShowResult(string statementText, Statement statement)
{
    try 
    {
        Debug.WriteLine("\t{0} => {1}",statementText, statement());
    }
    catch(Exception e)
    {
        Debug.WriteLine("\t{0} => throws {1}",statementText, e.GetType());
    }
}

class Demo
{
    Guid id;
    public Demo(Guid id) { this.id = id; }
    public override bool Equals(object obj)
    {
        return Equals(obj as Demo); //if objects are of non-comparable types, obj will be converted to null
    }
    public bool Equals(Demo obj)
    {
        if (obj == null)
        {
            return false;
        }
        else
        {
            return id.Equals(obj.id);
        }
    }
    //if two objects are Equal their hashcodes must be equal
    //however, if two objects hash codes are equal it is not necessarily true that the objects are equal
    //i.e. equal objects are a subset of equal hashcodes
    //more info here: https://dev59.com/yXRC5IYBdhLWcg3wOeSB#371348
    public override int GetHashCode()
    {
        return id.GetHashCode();
    }
}

class Demo2
{
    Guid id;
    public Demo2(Guid id)
    {
        this.id = id;
    }

    public static bool operator ==(Demo2 obj1, Demo2 obj2)
    {
        if (ReferenceEquals(null, obj1)) 
        {
            return ReferenceEquals(null, obj2); //true if both are null; false if only obj1 is null
        }
        else
        {
            if(ReferenceEquals(null, obj2)) 
            {
                return false; //obj1 is not null, obj2 is; therefore false
            }
            else
            {
                return obj1.id == obj2.id; //return true if IDs are the same; else return false
            }
        }
    }

    // NB: We also HAVE to override this as below if overriding the == operator; this is enforced by the compiler.  However, oddly we could choose to override it different to the below; but typically that would be a bad idea...
    public static bool operator !=(Demo2 obj1, Demo2 obj2)
    {
        return !(obj1 == obj2);
    }
}

1
在Object类中,Equals实现的是身份(identity)而非相等性(equality)。它检查引用是否相等。代码可能如下所示:
public virtual Boolean Equals(Object other) {
    if (this == other) return true;
    return false;
}

在实现类的.Equals方法时,只有在基类不是Object时才应调用基类的.Equals方法。是的,这很复杂。
更进一步地,由于派生类可以重写.Equals方法,因此你不能调用它来检查标识,Microsoft添加了静态.ReferenceEquals方法。
如果你使用某个类,则在逻辑上.Equals检查相等性,而.ReferenceEquals检查标识。

-4

Equals() 根据基础类型(值/引用)检查哈希码或等价性,而 ReferenceEquals() 则旨在始终检查哈希码。如果两个对象指向相同的内存位置,则 ReferenceEquals 返回 true

double e = 1.5;
double d = e;
object o1 = d;
object o2 = d;

Console.WriteLine(o1.Equals(o2)); // True
Console.WriteLine(Object.Equals(o1, o2)); // True
Console.WriteLine(Object.ReferenceEquals(o1, o2)); // False

Console.WriteLine(e.Equals(d)); // True
Console.WriteLine(Object.Equals(e, d)); // True
Console.WriteLine(Object.ReferenceEquals(e, d)); // False

3
这是胡说八道。Equals和ReferenceEquals都不考虑HashCode。唯一的要求是,相等的对象必须具有相等的HashCode。而且对象并没有指向任何地方...只有当两个参数都是同一个引用对象或都为null时,ReferenceEquals才为true。 - Jim Balter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接