string.Equals()和==运算符真的一样吗?

241

它们真的是相同的吗?今天我遇到了这个问题。这里是从立即窗口中获取的转储:

?s 
"Category" 
?tvi.Header 
"Category" 
?s == tvi.Header 
false 
?s.Equals(tvi.Header) 
true 
?s == tvi.Header.ToString() 
true 
所以,stvi.Header都包含"Category",但==返回false,Equals()返回true。 s被定义为字符串,tvi.Header实际上是WPF的TreeViewItem.Header。那么为什么它们返回不同的结果?我一直以为在C#中它们是可以互换的。有人能解释一下为什么吗?

我认为 string.Equals 匹配整个对象。 - Umair A.
1
是的,我从我的代码内部得到了相同的结果。实际上,这引起了我的注意,因为==在代码中返回false。我总是使用==进行字符串比较。今天早上,当我看到两侧都包含相同的字符串“Category”时,我简直不敢相信==会返回false(我甚至让我的同事再次检查是否有错)。但是当我改用Equals时它可以正常工作(与立即窗口显示的结果相同)。 - miliu
1
对于大多数字符串比较,您应该考虑调用包括 StringComparison 参数的 string.Equals 重载。对于编码字符串(例如 XML 属性),请使用 InvarientCulture 版本;对于用户输入的字符串,请使用 CurrentCulture 版本。这将处理许多 == 忽略的细节,例如 Unicode 字符规范化形式等,并使大小写敏感性明确。 - Jeffrey L Whitledge
8
@Robaticus,我不明白你在说什么。 - miliu
.Equals方法查看字符串的内容,而==运算符则比较对象。只有它们实际上是同一个对象时,==才会返回true。这在内存池技术中可能会变得棘手,因为如果您有字符串a =“test”和字符串b =“test”,它们实际上是同一个对象,并且在使用==进行比较时将返回true。 - Kyle
1
只有当对象的静态类型为字符串时,==才起作用。因为运算符重载只考虑静态类型。 - CodesInChaos
8个回答

368

两个不同之处:

  • Equals is polymorphic (i.e. it can be overridden, and the implementation used will depend on the execution-time type of the target object), whereas the implementation of == used is determined based on the compile-time types of the objects:

      // Avoid getting confused by interning
      object x = new StringBuilder("hello").ToString();
      object y = new StringBuilder("hello").ToString();
      if (x.Equals(y)) // Yes
    
      // The compiler doesn't know to call ==(string, string) so it generates
      // a reference comparision instead
      if (x == y) // No
    
      string xs = (string) x;
      string ys = (string) y;
    
      // Now *this* will call ==(string, string), comparing values appropriately
      if (xs == ys) // Yes
    
  • Equals will throw an exception if you call it on null, == won't

      string x = null;
      string y = null;
    
      if (x.Equals(y)) // NullReferenceException
    
      if (x == y) // Yes
    
请注意,您可以使用object.Equals来避免后者成为问题:
if (object.Equals(x, y)) // Fine even if x or y is null

4
“x == y” 的结果是 false,因为你使用了对象类的相等运算符检查引用相等性。“(string)x == (string)y” 在至少 .Net 4.0 中返回 true。请注意,这里使用了类型转换将对象转换为字符串进行比较。 - Daniel A.A. Pelsmaeker
4
好的回答。为了避免空指针问题,使用String.Equals(x, y)而不是object.Equals(x, y)是否更合理? - Chaulky
@Chaulky:这怎么比object.Equals更能避免空问题呢?唯一的好处是它限制了两个值都必须是string类型。 - Jon Skeet
3
我指的是与 object.Equals(x, y) 相比,这种类型检查是它的优势。问题是关于字符串比较的,因此使用 String.Equals() 的优点似乎在于增加了类型检查。避免进行类型检查是否更好?另外,感谢您回答一年前的评论! - Chaulky
2
@Chaulky:没有特别的原因-我知道 object.Equals 始终可用于避免空引用问题,而对于特定类型,我需要检查文档:) - Jon Skeet
显示剩余5条评论

63

问题中出现的表面矛盾是因为在一个情况下,Equals函数被调用在一个string对象上,而在另一种情况下,==操作符被调用在System.Object类型上。stringobject在实现相等性方面有不同(分别是值和引用)。

除此之外,任何类型都可以定义不同的==Equals,所以它们通常不能互换使用。

以下是使用double的示例(摘自Joseph Albahari关于C#语言规范第7.9.2节的注释):

double x = double.NaN;
Console.WriteLine (x == x);         // False
Console.WriteLine (x != x);         // True
Console.WriteLine (x.Equals(x));    // True
他接着说,double.Equals(double) 方法被设计为能够正确处理列表和字典。另一方面,== 运算符被设计为遵循 IEEE 754 浮点类型标准。
在确定字符串相等性的特定情况下,行业偏好通常都不使用 ==string.Equals(string)。这些方法确定两个字符串是否完全相同,而这很少是正确的行为。最好使用 string.Equals(string, StringComparison),它允许你指定特定类型的比较。通过使用正确的比较,你可以避免很多潜在的(非常难以诊断的)错误。
这里是一个例子:
string one = "Caf\u00e9";        // U+00E9 LATIN SMALL LETTER E WITH ACUTE
string two = "Cafe\u0301";       // U+0301 COMBINING ACUTE ACCENT
Console.WriteLine(one == two);                                          // False
Console.WriteLine(one.Equals(two));                                     // False
Console.WriteLine(one.Equals(two, StringComparison.InvariantCulture));  // True

在这个例子中,两个字符串看起来是一样的("Café"),所以如果使用天真的(序数)相等性来比较,这可能会非常棘手。


45

C#有两个“相等”概念:EqualsReferenceEquals。 对于大多数你遇到的类,==运算符会使用其中一个(或两个),并且通常只在处理引用类型时测试ReferenceEquals(但string类是一个C#已经知道如何测试值相等的实例)。

  • Equals比较值。(即使两个独立的int变量不在内存中的同一位置,它们仍然可以包含相同的值。)
  • ReferenceEquals比较引用,并返回操作数是否指向内存中的同一对象。

示例代码:

var s1 = new StringBuilder("str");
var s2 = new StringBuilder("str");
StringBuilder sNull = null;

s1.Equals(s2); // True
object.ReferenceEquals(s1, s2); // False
s1 == s2 // True - it calls Equals within operator overload
s1 == sNull // False
object.ReferenceEquals(s1, sNull); // False
s1.Equals(sNull); // Nono!  Explode (Exception)

1
这并不完全正确... 尝试使用 == 比较两个实际的字符串对象,它们具有不同的引用但相同的值,你会得到 true。Jon Skeet 解释了这个问题... - Noldorin
1
@Noldorin:这就是我说的原因:“==”运算符有时必须在它们之间进行选择,并且通常在处理可空类型时选择“ReferenceEquals”。 string是C#不仅使用“ReferenceEquals”的可空类型的示例。 OP询问了关于string对象,但实际上需要了解一般的C#对象。 - palswim
1
@palswim在评论中写道:“字符串是可空类型的一个例子”,但可空类型是值类型的一个子类。字符串是引用类型。请参见规范的第1.3节。 - Conrad Frix
这个答案的主要问题在于认为有一个==运算符,其行为取决于其操作数。因为运算符是重载的,实际上有许多==运算符,每个运算符都有其实现者给出的任何行为。编译器根据两个操作数的编译时类型选择特定的运算符。规定该选择的规则在C#规范版本5.0的第7.3节“运算符”中描述。 - phoog
微软关于重写Equals和==运算符的指南可以提供一些阐述和进一步解释。 - palswim
显示剩余4条评论

17

TreeViewItemHeader属性是静态类型,类型为object

因此==返回false。您可以使用以下简单代码片段重现此问题:

object s1 = "Hallo";

// don't use a string literal to avoid interning
string s2 = new string(new char[] { 'H', 'a', 'l', 'l', 'o' });

bool equals = s1 == s2;         // equals is false
equals = string.Equals(s1, s2); // equals is true

这是WPF的问题吗?我刚开始学习WPF。在WinForm应用程序中,我以前从未遇到过这种问题。所以,我们应该始终使用Equals而不是==吗? - miliu
2
值得注意的是,当s1s2都声明为字符串时,s1 == s2变为s1.Equals(s2)。在C#中,字符串相等具有特殊含义。 - Jeff Mercado
@miliu:从我的示例中可以看出,这与WPF无关,但通常在.NET中是如此。 - Dirk Vollmar
我相信它实际上是拼写为“H-e-l-l-o”。 - Sergey

6
除了Jon Skeet的回答之外,我想解释一下为什么大多数情况下,当使用 == 时,您实际上会在具有相同值的不同字符串实例上得到答案 true
string a = "Hell";
string b = "Hello";
a = a + "o";
Console.WriteLine(a == b);

如您所见,ab必须是不同的字符串实例,但由于字符串是不可变的,运行时使用所谓的string interning来让ab引用相同的内存中的字符串。对象的==运算符检查引用,因为ab都引用相同的实例,所以结果是true。当您更改其中任何一个时,会创建一个新的字符串实例,这就是为什么字符串插入是可能的原因。
顺便说一下,Jon Skeet的答案并不完整。确实,x == yfalse,但这只是因为他在比较对象,对象按引用比较。如果您编写(string)x == (string)y,它将再次返回true。因此,字符串具有其==运算符重载,该运算符在底层调用String.Equals

12
我认为从回答中可以清楚地看出这一点,但我已经编辑过它,使其更加完整。然而,你关于字符串驻留的回答是不正确的。在上面,ab指向不同的实例;字符串驻留仅适用于编译时常量。你的代码打印True是因为调用了==重载函数,它比较字符序列。最终a所引用的字符串并没有被驻留。 - Jon Skeet
@Jon Skeet:为什么a最终引用的字符串没有被池化?是因为在比较时无法确定a的编译时值吗?初始值“Hell”会被池化吗? - Ax.
1
@Ax:是的 - 它不是编译时常量。 "Hell" 确实会被内部化。 (当然,由于 b,"Hello" 也会存在于内部池中;但 a 将是调用 String.Concat("Hell", "o"); 的结果。 - Jon Skeet
2
系统仅在明确请求时才对非常量字符串进行内部处理,部分原因是一旦字符串被内部处理,内部处理的副本就永远无法进行垃圾回收。如果经常使用包含特定32,000个字符序列的字符串,则可以通过对字符串进行内部处理来节省每个副本64K的空间。但是,如果内部处理一个32,000个字符的字符串,并且再也不使用该字符序列,则将永久浪费64K的内存。程序不能太多次地这样做。 - supercat
3
尽管系统在创建字符串时可以检查它是否在内部池中,而无需尝试将其添加到池中,但在大多数程序中生成的字符串很少会在内部池中。对于每个生成的字符串都检查内部池,以防万一其中有一个副本可能存在,通常会浪费更多时间而不是节省时间。 - supercat

4
这里已经有很多关于此问题的描述性答案了,所以我不会重复之前的内容。我想要添加的是以下代码,其中包含我所能想到的所有排列组合。由于组合数量较多,代码相对较长。您可以将其放入MSTest中并查看输出结果(输出结果包含在底部)。
这些证据支持Jon Skeet的回答。
代码:
[TestMethod]
public void StringEqualsMethodVsOperator()
{
    string s1 = new StringBuilder("string").ToString();
    string s2 = new StringBuilder("string").ToString();

    Debug.WriteLine("string a = \"string\";");
    Debug.WriteLine("string b = \"string\";");

    TryAllStringComparisons(s1, s2);

    s1 = null;
    s2 = null;

    Debug.WriteLine(string.Join(string.Empty, Enumerable.Repeat("-", 20)));
    Debug.WriteLine(string.Empty);
    Debug.WriteLine("string a = null;");
    Debug.WriteLine("string b = null;");

    TryAllStringComparisons(s1, s2);
}
private void TryAllStringComparisons(string s1, string s2)
{
    Debug.WriteLine(string.Empty);
    Debug.WriteLine("-- string.Equals --");
    Debug.WriteLine(string.Empty);
    Try((a, b) => string.Equals(a, b), s1, s2);
    Try((a, b) => string.Equals((object)a, b), s1, s2);
    Try((a, b) => string.Equals(a, (object)b), s1, s2);
    Try((a, b) => string.Equals((object)a, (object)b), s1, s2);

    Debug.WriteLine(string.Empty);
    Debug.WriteLine("-- object.Equals --");
    Debug.WriteLine(string.Empty);
    Try((a, b) => object.Equals(a, b), s1, s2);
    Try((a, b) => object.Equals((object)a, b), s1, s2);
    Try((a, b) => object.Equals(a, (object)b), s1, s2);
    Try((a, b) => object.Equals((object)a, (object)b), s1, s2);

    Debug.WriteLine(string.Empty);
    Debug.WriteLine("-- a.Equals(b) --");
    Debug.WriteLine(string.Empty);
    Try((a, b) => a.Equals(b), s1, s2);
    Try((a, b) => a.Equals((object)b), s1, s2);
    Try((a, b) => ((object)a).Equals(b), s1, s2);
    Try((a, b) => ((object)a).Equals((object)b), s1, s2);

    Debug.WriteLine(string.Empty);
    Debug.WriteLine("-- a == b --");
    Debug.WriteLine(string.Empty);
    Try((a, b) => a == b, s1, s2);
#pragma warning disable 252
    Try((a, b) => (object)a == b, s1, s2);
#pragma warning restore 252
#pragma warning disable 253
    Try((a, b) => a == (object)b, s1, s2);
#pragma warning restore 253
    Try((a, b) => (object)a == (object)b, s1, s2);
}
public void Try<T1, T2, T3>(Expression<Func<T1, T2, T3>> tryFunc, T1 in1, T2 in2)
{
    T3 out1;

    Try(tryFunc, e => { }, in1, in2, out out1);
}
public bool Try<T1, T2, T3>(Expression<Func<T1, T2, T3>> tryFunc, Action<Exception> catchFunc, T1 in1, T2 in2, out T3 out1)
{
    bool success = true;
    out1 = default(T3);

    try
    {
        out1 = tryFunc.Compile()(in1, in2);
        Debug.WriteLine("{0}: {1}", tryFunc.Body.ToString(), out1);
    }
    catch (Exception ex)
    {
        Debug.WriteLine("{0}: {1} - {2}", tryFunc.Body.ToString(), ex.GetType().ToString(), ex.Message);
        success = false;
        catchFunc(ex);
    }

    return success;
}

输出:

string a = "string";
string b = "string";

-- string.Equals --

Equals(a, b): True
Equals(Convert(a), b): True
Equals(a, Convert(b)): True
Equals(Convert(a), Convert(b)): True

-- object.Equals --

Equals(a, b): True
Equals(Convert(a), b): True
Equals(a, Convert(b)): True
Equals(Convert(a), Convert(b)): True

-- a.Equals(b) --

a.Equals(b): True
a.Equals(Convert(b)): True
Convert(a).Equals(b): True
Convert(a).Equals(Convert(b)): True

-- a == b --

(a == b): True
(Convert(a) == b): False
(a == Convert(b)): False
(Convert(a) == Convert(b)): False
--------------------

string a = null;
string b = null;

-- string.Equals --

Equals(a, b): True
Equals(Convert(a), b): True
Equals(a, Convert(b)): True
Equals(Convert(a), Convert(b)): True

-- object.Equals --

Equals(a, b): True
Equals(Convert(a), b): True
Equals(a, Convert(b)): True
Equals(Convert(a), Convert(b)): True

-- a.Equals(b) --

a.Equals(b): System.NullReferenceException - Object reference not set to an instance of an object.
a.Equals(Convert(b)): System.NullReferenceException - Object reference not set to an instance of an object.
Convert(a).Equals(b): System.NullReferenceException - Object reference not set to an instance of an object.
Convert(a).Equals(Convert(b)): System.NullReferenceException - Object reference not set to an instance of an object.

-- a == b --

(a == b): True
(Convert(a) == b): True
(a == Convert(b)): True
(Convert(a) == Convert(b)): True

3

很明显,tvi.header不是一个String类型。 ==是由String类重载的运算符,这意味着它只有在编译器知道运算符两侧都是String类型时才能正常工作。


-1
一个对象由一个唯一的OBJECT_ID定义。如果A和B是对象,且A == B为真,则它们是完全相同的对象,它们具有相同的数据和方法,但是这也是正确的:
A.OBJECT_ID == B.OBJECT_ID
如果A.Equals(B)为真,则意味着两个对象处于相同的状态,但这并不意味着A与B完全相同。
字符串是对象。
请注意,==和Equals运算符是自反的、对称的、传递的,因此它们是等价关系(使用关系代数术语)。
这是什么意思: 如果A、B和C是对象,则:
(1)A == A总是为真;A.Equals(A)总是为真(自反性)
(2)如果A == B那么B == A;如果A.Equals(B),则B.Equals(A)(对称性)
(3)如果A == B且B == C,则A == C;如果A.Equals(B)且B.Equals(C),则A.Equals(C)(传递性)
此外,您还可以注意到这也是正确的:
(A == B)=>(A.Equals(B)),但反之则不成立。
A B =>
0 0 1
0 1 1
1 0 0
1 1 1

现实生活中的例子: 两个相同类型的汉堡包具有相同的属性:它们是Hamburger类的对象,其属性完全相同,但它们是不同的实体。如果你买了这两个汉堡包并吃了其中一个,另一个不会被吃掉。所以Equals和==之间的区别在于: 你有hamburger1和hamburger2。它们处于完全相同的状态(重量、温度、味道都一样),因此hamburger1.Equals(hamburger2)为true。但是hamburger1 == hamburger2为false,因为如果hamburger1的状态发生改变,hamburger2的状态不一定会改变,反之亦然。
如果你和朋友买了一个汉堡,这个汉堡既属于你又属于他,那么你必须决定将汉堡分成两份,因为you.getHamburger() == friend.getHamburger()为true,如果发生这种情况:friend.eatHamburger(),那么你的汉堡也会被吃掉。
我可以写其他关于Equals和==的微妙之处,但我现在很饿,所以我得走了。
最好的问候, Lajos Arpad。

2
我想知道这个踩的原因(我仍然不明白我的评论哪里错了)。也许踩我的人可以告诉我他认为我哪里说错了。 - Lajos Arpad
1
不是我给你的投票,但你的帖子中有几个错误可能导致了这种情况。A == A并不能保证A.Equals(A)对于对象来说只是两个方法调用,这些调用的结果不能保证完全相同,进一步地(2)如果A == B那么B == A;如果A.Equals(B)那么B.Equals(A)(对称性)也不成立,因为A和B可能具有两种不同的运行时类型,每种类型都有自己的Equals重载,产生不同的结果,最后A == B和B == C并不能导致A == C。看看这段代码:string A = "foo"; string B = "foo"; object C = B; A == C是false。 - Rune FS
关于对称性:我非常渴望看到一个例子,其中A == B为真,而B == A为假。请注意,我们谈论的是A和B作为对象,我们不是在谈论A == B在某个时间为真,然后更改B并使B == A在以后为假,我们谈论的是A == B在同一时间为真,而B == A为假。 - Lajos Arpad
关于传递性:很遗憾,您的示例是微不足道的错误:string A =“foo”;string B =“foo”;Object C = B;这导致A == B为false,A.Equals(B)为true,因此首先A == B和B == C都是false。我可以看出您和miliu有同样的问题,即不理解Equals和==之间的区别,至少他没有在注释中写误导性的示例。 - Lajos Arpad
1
在C#中,“==”符号用于表示两个不同的运算符:可重载(但非虚拟)相等运算符和固定的引用相等运算符。如果使用“==”运算符来比较已明确定义了重载的类型组合(例如比较两个字符串),它将使用该重载。因此,对于两个String类型的参数进行比较将检查字符串是否具有相同的内容,而String和Object之间的比较将检查其参数的对象ID。 - supercat
显示剩余4条评论

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