字符串常量池?

9
第二个ReferenceEquals调用返回false。为什么s4中的字符串没有被interned?(我不关心StringBuilder比字符串拼接的优势。)
string s1 = "tom";
string s2 = "tom";


Console.Write(object.ReferenceEquals(s2, s1)); //true

string s3 = "tom";
string s4 = "to";
s4 += "m";

Console.Write(object.ReferenceEquals(s3, s4)); //false

当我执行String.Intern(s4);时,仍然返回false。
在这里,s3和s4都被interned,但它们的引用不相等?
string s3 = "tom";
string s4 = "to";
s4 += "m";
String.Intern(s4);

Console.WriteLine(s3 == s4); //true
Console.WriteLine(object.ReferenceEquals(s3, s4)); //false
Console.WriteLine(string.IsInterned(s3) != null);  //true (s3 is interned)
Console.WriteLine(string.IsInterned(s4) != null);  //true (s4 is interned)

请再次验证 s4 = String.Intern(s4); Console.Write(object.ReferenceEquals(s3, s4)); 对于.NET 2.0、3.0、3.5、4.0返回true。此外,如果您测试s3 = String.Intern(s3); Console.Write(object.ReferenceEquals(s3, s1)); 您会发现s3 = String.Intern(s3); 不起作用,因为像Scott Dorman正确写的那样,从s1到s3的所有内容都已经被内部化了,只有s4指向一个唯一的堆指针,在我们使用s4 = String.Intern(s4); 更改它之前。 - Oleg
string.Interned() 并不意味着传入的字符串对象是作为 interned string 创建的,它意味着在 interned store 中有一个具有相同值的字符串。混淆了,是吧! - Ian Mercer
有道理。但是String.Intern(s4)不会将字符串池化吗? - rkrauter
是的,它确实会将字符串放入内部池中,但您仍然没有比较内部化的引用。请查看我的答案更新以获取更多信息。来自 MSDN 的说明:Intern 方法使用内部池来搜索与 str 值相等的字符串。如果存在这样的字符串,则返回其在内部池中的引用。如果该字符串不存在,则将对 str 的引用添加到内部池中,然后返回该引用。 - Scott Dorman
6个回答

18

s4中的字符串是被内部化的。但是,当你执行s4 += "m";时,你创建了一个新的字符串,它的值不是一个字符串文本,而是一个字符串连接操作的结果,因此s3s4是两个不同的字符串实例,存储在两个不同的内存位置。

关于字符串内部化的更多信息,请查看此处,特别是最后一个示例。当你执行String.Intern(s4)时,确实将字符串内部化,但仍未执行这两个已内部化字符串之间的引用相等性测试。 String.Intern方法返回内部化的字符串,因此你需要执行以下步骤:

string s1 = "tom";
string s2 = "tom";

Console.Write(object.ReferenceEquals(s2, s1)); //true 

string s3 = "tom";
string s4 = "to";
s4 += "m";

Console.Write(object.ReferenceEquals(s3, s4)); //false

string s5 = String.Intern(s4);

Console.Write(object.ReferenceEquals(s3, s5)); //true

1
标记为答案,谢谢。还有一些奇怪的东西。我告诉它将 s4 托管并返回到池中已经托管的字符串的引用。而可怜的 s4 只是留在堆中作为一个非托管字符串。 - rkrauter
1
谢谢。它返回一个引用,但如果作为参数传递的字符串值尚未被内部化,则会将其内部化,然后返回引用。字符串内部化实际上是一种优化技术,主要由编译器用于减少应用程序中字符串实例的数量。除非您正在创建大量字符串,否则自己进行内部化可能不会带来太多好处。 - Scott Dorman

3

字符串是不可变的,这意味着它们的内容无法更改。

当您执行s4 += "m";时,CLR会将字符串复制到内存中的另一个位置,其中包含原始字符串和追加部分。

请参阅MSDN字符串参考


我知道字符串是不可变的。但是整个interning的目的不就是为了节省内存吗?为什么CLR不能说,嘿,我在我的intern池中有这个相同的值,我只需要指向它。 - rkrauter
7
在每个操作之后检查内部池中的所有字符串是否与操作结果相等,这样做成本很高!因此,CLR为了提高执行速度而牺牲了内存效率。在编译时进行的字符串计算可能会很慢,因此其结果可以被放入内部池中。但运行时的计算必须快速完成,因此检查每个结果是否与一系列其他字符串相等是不可行的。 - Vlad
那么字符串驻留主要是在编译时完成的吗?我刚刚注意到当我执行String.Intern(s4);时,仍然返回false。请解释一下。 - rkrauter

2

来源:https://blogs.msdn.microsoft.com/ericlippert/2009/09/28/string-interning-and-string-empty/

字符串驻留是编译器的优化技术。如果在一个编译单元中有两个相同的字符串字面量,则生成的代码确保该程序集中所有该文字(用双引号括起来的字符)实例只创建一个字符串对象。

我来自C#背景,可以通过一个例子来解释:

object obj = "Int32";
string str1 = "Int32";
string str2 = typeof(int).Name;

以下是比较的输出结果:
Console.WriteLine(obj == str1); // true
Console.WriteLine(str1 == str2); // true    
Console.WriteLine(obj == str2); // false !?
注意1:对象按引用比较。 注意2:typeof(int).Name通过反射方法评估,因此它不会在编译时得到评估。 在这里,这些比较是在编译时进行的。 结果分析: 1)正确,因为它们都包含相同的文字,所以生成的代码只有一个对象引用“Int32”。见注意1
2)正确,因为检查了两个值的内容,它们是相同的。
3)错误,因为str2和obj没有相同的文本。请参见注意2

2

首先,目前为止关于不可变字符串的所有内容都是正确的。但是还有一些重要的事情没有被提到。以下是代码:

string s1 = "tom";
string s2 = "tom";
Console.Write(object.ReferenceEquals(s2, s1)); //true

显示结果确实是“True”,但这只是由于一些小的编译器优化或者像这里一样,因为CLR忽略了C#编译器属性(参见“CLR via C#”书),并且在堆中仅放置了一个字符串"tom"

其次,您可以通过以下代码来解决这种情况:

s3 = String.Intern(s3);
s4 = String.Intern(s4);
Console.Write (object.ReferenceEquals (s3, s4)); //true

String.Intern 函数计算字符串的哈希码并在内部哈希表中搜索相同的哈希码。因为找到了,它会返回对已存在的 String 对象的引用。如果字符串不存在于内部哈希表中,则会创建该字符串的副本并计算哈希值。垃圾回收器不会释放字符串的内存,因为它被哈希表引用。


1
在C#中,每个字符串都是一个独立的对象,不能被编辑。你可以创建对它们的引用,但每个字符串都是独立的。这种行为是一致的,易于理解。
我建议您查看StringBuilder类来操作字符串而不创建新实例。它应该足以满足您想要做的任何与字符串相关的操作。

1
只有在需要连接大字符串时才使用StringBuilder。在所有其他情况下,节省的时间是如此微小,以至于无关紧要。此外,字符串被放置在已经创建的字符串数组中,这意味着,如果您创建了字符串“Hello”,任何进一步的字符串“Hello”都将指向内存中相同的引用。 - Femaref

0

当比较两个对象而不是字符串时,不会调用字符串相等运算符,因为它是一个没有多态性的静态方法。

这里有一个测试:

static void Test()
{
  object o1 = "a";
  object o2 = new string("a".ToCharArray());

  string o3 = "a";
  string o4 = new string("a".ToCharArray());

  object o5 = "a"; // Compiler optimization addr(o5) = addr(o6)
  object o6 = "a";

  string o7 = "a"; // Compiler optimization addr(o7) = addr(o8)
  string o8 = "a";

  Console.WriteLine("Enter same text 4 times:");

  object o9 = Console.ReadLine();
  object o10 = Console.ReadLine();

  string o11 = Console.ReadLine();
  string o12 = Console.ReadLine();

  Console.WriteLine("object arr   o1  == o2  ? " + ( o1 == o2 ).ToString());
  Console.WriteLine("string arr   o3  == o4  ? " + ( o3 == o4 ).ToString());
  Console.WriteLine("object const o5  == o6  ? " + ( o5 == o6 ).ToString());
  Console.WriteLine("string const o7  == o8  ? " + ( o7 == o8 ).ToString());
  Console.WriteLine("object cnsl  o9  == o10 ? " + ( o9 == o10 ).ToString());
  Console.WriteLine("string cnsl  o11 == o12 ? " + ( o11 == o12 ).ToString());
  Console.WriteLine("o1.Equals(o2) ? " + o1.Equals(o2).ToString());
  Console.WriteLine("o3.Equals(o4) ? " + o3.Equals(o4).ToString());
  Console.WriteLine("o5.Equals(o6) ? " + o5.Equals(o6).ToString());
  Console.WriteLine("o7.Equals(o8) ? " + o7.Equals(o8).ToString());
  Console.WriteLine("o9.Equals(o10) ? " + o9.Equals(o11).ToString());
  Console.WriteLine("o11.Equals(o12) ? " + o11.Equals(o12).ToString());
}

结果:

object arr   o1  == o2  ? False
string arr   o3  == o4  ? True
object const o5  == o6  ? True
string const o7  == o8  ? True
object cnsl  o9  == o10 ? False
string cnsl  o11 == o12 ? True
o1.Equals(o2) ? True
o3.Equals(o4) ? True
o5.Equals(o6) ? True
o7.Equals(o8) ? True
o9.Equals(o10) ? True
o11.Equals(o12) ? True

https://referencesource.microsoft.com/#mscorlib/system/string.cs


@CodeCaster 好的,谢谢你帮助我提高自己。 - user12031933

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