关于c#:string.Equals()和==运算符是否真的相同?

Are string.Equals() and == operator really same?

本问题已经有最佳答案,请猛点这里访问。

它们真的一样吗?今天,我遇到了这个问题。下面是即时窗口中的转储:

1
2
3
4
5
6
7
8
9
10
?s
"Category"
?tvi.Header
"Category"
?s == tvi.Header
false
?s.Equals(tvi.Header)
true
?s == tvi.Header.ToString()
true

因此,stvi.Header都包含"类别",但==返回假,Equals()返回真。

s定义为字符串,tvi.Header实际上是wpf TreeViewItem.Header。那么,他们为什么返回不同的结果呢?我一直认为它们可以在C中互换。

有人能解释这是为什么吗?


两个不同点:

  • Equals是多态的(即它可以被重写,使用的实现将取决于目标对象的执行时间类型),而使用的==的实现是根据对象的编译时间类型确定的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 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将爆炸,==不会

    1
    2
    3
    4
    5
    6
    string x = null;
    string y = null;

    if (x.Equals(y)) // Bang

    if (x == y) // Yes

请注意,使用object.Equals可以避免后者出现问题:

1
if (object.Equals(x, y)) // Fine even if x or y is null


问题中出现的明显矛盾是由于在一种情况下,Equals函数在string对象上调用,而在另一种情况下,==运算符在System.Object类型上调用。stringobject实现的平等不同(分别是值和参考值)。

除此之外,任何类型都可以以不同的方式定义==Equals,因此一般来说,它们不可互换。

下面是一个使用double的例子(摘自Joseph Albahari在C语言规范第7.9.2节中的注释):

1
2
3
4
double x = double.NaN;
Console.WriteLine (x == x);         // False
Console.WriteLine (x != x);         // True
Console.WriteLine (x.Equals(x));    // True

他接着说,double.Equals(double)方法的设计是为了正确地处理列表和字典。另一方面,==运算符的设计遵循了适用于浮点类型的IEEE754标准。

在确定字符串相等的特定情况下,行业偏好大多数时候既不使用==,也不使用string.Equals(string)。这些方法确定两个字符串对于字符是否是同一个字符,这很少是正确的行为。最好使用string.Equals(string, StringComparison),它允许您指定特定的比较类型。通过使用正确的比较,您可以避免许多潜在的(很难诊断的)错误。

下面是一个例子:

1
2
3
4
5
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_"),所以如果使用na,调试起来可能非常困难。ve(序数)相等。


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

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

示例代码:

1
2
3
4
5
6
7
8
9
10
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)


TreeViewItemHeader属性静态类型为object类型。

因此,==产生false。您可以使用以下简单的代码片段来重现:

1
2
3
4
5
6
7
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


这里有很多描述性的答案,所以我不会重复已经说过的话。我想添加的是下面的代码,演示我能想到的所有排列。由于组合的数量,代码相当长。您可以随意将其放入MSTEST,自己查看输出(输出包含在底部)。

这些证据支持乔恩·斯基特的回答。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
[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;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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

除了jon skeet的答案外,我想解释一下为什么在使用==的大多数时间里,您实际上在具有相同值的不同字符串实例上得到了true的答案:

1
2
3
4
string a ="Hell";
string b ="Hello";
a = a +"o";
Console.WriteLine(a == b);

如您所见,ab必须是不同的字符串实例,但由于字符串是不可变的,所以运行时使用所谓的字符串interning让ab引用内存中的同一字符串。对象的==操作符检查引用,由于ab引用同一实例,结果是true。当您更改其中任何一个时,都会创建一个新的字符串实例,这就是为什么可以进行字符串实习生。

顺便说一下,乔恩·斯基特的回答不完整。的确,x == y就是false,但这仅仅是因为他比较的是参照物和参照物。如果你写了(string)x == (string)y,它会再次返回true。所以字符串的==-操作符被重载,在下面调用String.Equals


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


对象由唯一的对象ID定义。如果A和B是物体,并且A==B为真,则它们是非常相同的对象,它们具有相同的数据和方法,但这也是真的:

a.object_id==b.object_id

如果a.等于(b)是真的,这意味着两个物体处于相同的状态,但这并不意味着a和b是完全相同的。

字符串是对象。

注意,==和等于运算符是自反的、同余的、转换的,因此它们是等价关系(使用关系代数项)

这意味着:如果A、B和C是对象,则:

(1)a==a始终为真;a.equals(a)始终为真(自反性)

(2)如果a=b,则b=a;如果a.等于(b),则b.等于(a)(同时)

(3)如果a==b和b==c,则a==c;如果a.等于(b)和b.等于(c),则a.等于(c)(变率)

此外,您还可以注意到,这也是正确的:

(a==b)=>(a.等于(b)),但倒数不是真的。

1
2
3
4
5
A B =>
0 0 1
0 1 1
1 0 0
1 1 1

现实生活的例子:同一类型的两个汉堡具有相同的属性:它们是汉堡类的对象,它们的属性完全相同,但它们是不同的实体。如果你买这两个汉堡包,一个吃,另一个就不吃了。所以,等号和==之间的区别是:你有汉堡1和汉堡2。它们处于完全相同的状态(同样的重量,同样的温度,同样的味道),所以汉堡等于汉堡。但汉堡包1==汉堡包2是错误的,因为如果汉堡包1的状态发生变化,汉堡包2的状态就不一定发生变化,反之亦然。

如果你和一个朋友同时得到一个汉堡包,这是你和他的,那么你必须决定把汉堡包分成两部分,因为你.getHamburger()==friend.getHamburger()是真的,如果发生这种情况:friend.eatHamburger(),那么你的汉堡包也会被吃掉。

我可以写一些关于平等和==的细微差别,但我饿了,所以我得走了。

最好的问候,Lajos Arpad。