在C#中比较字符串和对象

73

看看这段代码:

object x = "mehdi emrani";
string y = "mehdi emrani";
Console.WriteLine(y == x);

返回true的代码。

但是这段代码:

object x = "mehdi emrani";
string y = "mehdi ";
y += "emrani";
Console.WriteLine(y == x);

返回 false

所以,当我在第一个代码中比较 String 和 Object 时,结果为true
但是,当我在第二个代码中比较它们时,结果为false

两个字符串相同,但为什么当我向字符串附加内容时,结果返回false


比较字符串时,应始终使用.Equals函数。 - RononDex
3
他说为什么? - Mohammad
11
@RononDex说的是Java语言的情况,但对于C#语言来说并不完全正确。通常情况下,你会比较字符串,然后使用重载的==运算符。这种方法的优点在于它可以正确处理null的情况。 - Tim Schmelter
1
仔细阅读C#编译器的警告信息。它们试图帮助并指导你。在你的情况下,你会得到警告CS0253:可能意外的引用比较;为了获得值比较,请将右侧转换为类型“string”。这应该让你警觉,并给你所需的解释。 - Jeppe Stig Nielsen
6个回答

96
在每种情况下,==的第二个操作数是类型为objectx。这意味着您正在使用普通的引用相等运算符。
现在在第一个情况下,您正在使用两个内容相同的字符串常量。 C#编译器将为这两个引用使用单个对象。在第二种情况下,xy指向具有相同内容的不同字符串对象。这两个引用将不同,因此==将返回false。
您可以通过以下方法修复比较:
  • 使用Equals - 它被string覆盖(与仅重载的==运算符不同):

    Console.WriteLine(y.Equals(x)); // or x.Equals(y), or Equals(y, x)
    

    如果两个参数中任意一个可能为null,使用静态的Equals(object, object)方法很有用;这意味着你不需要担心NullReferenceException

  • 将两个变量都声明为string类型,这时候编译器会在编译时选择string内部重载的==操作符,这个操作符比较的是字符串的内容而非引用。

值得注意的是,C#编译器并不只是识别字符串字面量本身,它还能处理编译时常量表达式。例如:

object x = "mehdi emrani";
string y = "mehdi " + "emrani";
Console.WriteLine(y == x); // True

这里使用了两个不同于初始化 x 的字符串文字来初始化 y,但是编译器执行了字符串连接,意识到它与已经用于 x 的字符串是相同的。


我认为提到“字符串驻留(string interning)”这个表达式是合适的,这也是 OP 的第一个例子能够成功比较的原因,尽管它们被作为对象进行比较,但它们实际上指向同一个对象。这补充了第一个使用 == 运算符比较对象应该被避免的观点。 - flindeberg
3
我避免使用"interning"这个术语,因为它通常被认为是一个执行时操作,而这是一个编译时操作——编译器只会生成一个字符串,所以CLR不需要对任何内容进行"interning"。 - Jon Skeet
同时,Console.WriteLine( string.Intern(y) == x ); 输出 true。在你已经知道 x 被 Interned 的情况下。 - Panos Theof
@PanosTheof 你的意思是 Console.WriteLine((object)string.IsInterned(y) == x); 吗?如果是,它会输出 True。然而,在原问题的第二种情况下,Console.WriteLine((object)string.IsInterned(y) == (object)y); 输出 False。它们,即 xy,是不同的实例,其中一个实例在字符串内部池中。 - Jeppe Stig Nielsen
a) 是的,它与string.Intern()相同,而且可能更正确。 b) 在第二种情况中,他将y与x进行比较,而不是将y与y进行比较。此外,x的类型为object,不需要进行(object)转换。 - Panos Theof
其中一个对象与其他不同! - John Odom

33

当你初始化时

object x = "mehdi emrani";  //pointer(x)

它在内存中初始化并将引用分配给x。之后,当您初始化时,

string y = "mehdi emrani"; //pointer(x)

编译器发现该值已经存在于内存中,因此将相同的引用分配给y。
现在,实际上比较地址而不是值的等于运算符找到了两个变量的相同地址,结果为true。
x==y  //actually compares pointer(x)==pointer(x) which is true

在第二种情况下,当您初始化x和y时,它们被分配不同的地址。
object x = "mehdi emrani";  //Pointer(x)
string y = "mehdi ";        //not found in memory
y += "emrani";              //Pointer(y)

现在进行比较,发现不同的地址导致结果为假:

x == y  //is actually Pointer(x) == Pointer(y) which is false

为了解决这个问题,你需要使用.Equals()方法。这个方法不仅比较引用,还比较值和对象类型。
Console.WriteLine(y.Equals(x));   //compares "mehdi emrani" == "mehdi emrani" results true

你确定如果将一个字面字符串值分配给一个字符串,它会检查内存中是否已经有相同的字符串并分配该指针吗?如果是这样,我认为这是一个糟糕的决定。如果我执行 y += " veci"; 这也会改变 x。我想你的意思是说 y = x; 这将设置指针。 - Chad
@Chad 1) 所有相等的字符串常量都共享同一个实例,因为它们被内部化了。这包括通过连接形成的常量。但是它不包括在运行时发生的连接。2) += 不会改变左侧的值。它创建一个新实例并将其分配给变量。在 C# 中,x += yx = x + y 非常相似(与 C++ 不同,后者可以是一个独立的变异操作)。 - CodesInChaos
@CodesinChaos - 你有任何参考资料可以支持并提供更多阅读材料吗?如果这是真的,我认为完全理解它是非常重要的。 - Chad

7

很可能是比较引用(对象的标准Equals实现)。在第一个示例中,C#优化了常量字符串,因此y和x实际上指向同一个对象,因此它们的引用相等。而在另一种情况下,y是动态创建的,因此引用不同。


7
在第一种情况下,.NET执行字符串常量优化并仅分配一个String实例。x和y都指向同一个对象(两个引用相等)。
但是在第二种情况下,x和y指向不同的String实例。将"ermani"添加到y会创建第三个字符串对象。
"=="运算符基本上返回true,如果两边的操作数引用同一个对象。在第一种情况下,x和y引用同一个对象,在第二种情况下,x和y引用不同的对象。

5
每次修改现有字符串时,都会在后台创建一个新的字符串,因为字符串是不可变的,这意味着它们不能更改。
请参见以下说明:.NET字符串为什么是不可变的?

4
在我看来,这并没有真正解释正在发生的事情。 - Jon Skeet

5

你尝试过以下方法吗:

Console.WriteLine(y == x.ToString());

那会有什么作用呢?这些对象已经是字符串了,只需使用 String.ToString() 就可以返回原始字符串。 - Panagiotis Kanavos
2
@PanagiotisKanavos:确实,这样做可以解决问题,因为编译器知道x.ToString返回一个字符串,因此它使用字符串的重载==运算符来比较内容而不是引用。但是,我在这个答案中还缺少更多细节。 - Tim Schmelter
1
那么,首先使用一个简单的转换或者直接使用字符串变量。 - Panagiotis Kanavos
@PanagiotisKanavos:但在这里它是一个对象,另外,String.ToString 是一个空操作(return this;)。 - Tim Schmelter
@PanagiotisKanavos == 运算符被字符串类重载,这意味着它是静态的。因此,只有操作数的静态类型才有关系,而不是运行时类型。 - flindeberg
编译原始案例和我的建议,看看它们之间的区别。"x"是对象,如果没有ToString(),则比较结果不是期望的结果。 - i486

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