StringBuilder字符串是不可变的吗?

6

StringBuilder被认为是比简单字符串连接更快的字符串操作工具。无论真假如何,我想知道StringBuilder操作的结果和它们产生的字符串。

快速查看反编译器可以发现,StringBuilder.ToString()并不总是返回一个副本,有时它似乎返回内部字符串的实例。它还似乎使用一些内部函数来操作内部字符串。

那么如果我这样做会得到什么?

string s = "Yo Ho Ho";
StringBuilder sb = new StringBuilder(s);
string newString = sb.ToString();
sb.Append(" and a bottle of rum.");
string newNewString = sb.ToString();

新字符串 newString 和新的新字符串 newNewString 是不同的字符串实例还是相同的?我已经尝试通过反射器来弄清楚这一点,但我并没有完全理解所有内容。
那么这段代码呢?
StringBuilder sb = new StringBuilder("Foo\n");
StringReader sr = new StringReader(sb.ToString());
string s = sr.ReadLine();
sb.Append("Bar\n");
s = sr.ReadLine();

最后一个语句会返回null还是"Bar"?如果返回其中之一,这是定义好的还是未定义的行为?换句话说,我能依赖它吗?

关于这个问题,文档非常简洁,因此我不愿意仅凭观察到的行为来进行依赖。


请注意,自此问题被提出并回答以来,StringBuilder的实现方式已经发生了变化。 - tymtam
4个回答

10

在mscorlib之外,任何System.String实例都是不可变的。

StringBuilder在内部对字符串进行一些有趣的操作,但归根结底,它不会将一个字符串返回给你,然后在你的代码中以可见的方式对其进行修改。

至于StringBuilder.ToString()后续调用是否返回相同的字符串实例或具有相同值的不同字符串实例,则取决于具体实现,您不应依赖此行为。


是的,实验表明这是正确的,但使用Reflector深入研究内部并没有显示出明显的原因。它似乎在每次调用时返回对内部字符串的引用,因此我对字符串是否可以变异感到困惑。 - Erik Funkenbusch
@Mystere,StringBuilder的实现一开始有点令人生畏。直到我看了你的问题并开始探索,我才意识到它们进行线程级缓存。我计划今晚(或明天)稍微再仔细看一下。但你可以相信生成的String是不可变的。 - JaredPar
@JaredPar - 尽管这在99.999%的情况下是正确的,但是您可以在mscorlib之外拥有一个可变字符串...只要该字符串被interned。 要点:https://gist.github.com/binarycow/304d7e23f0b1f55169e1069c829676c4 - Mike Christiansen

4

newStringnewNewString 是两个不同的字符串实例。

虽然 ToString() 返回当前字符串,但它会清除其当前线程变量。这意味着下一次追加时,它将在追加之前复制当前字符串。

我不是完全确定你在第二个问题中想要表达什么,但如果文件的最后字符是前一行的行终止符,则该行被认为没有空行位于这些字符和文件末尾之间。先前读取的字符串对此没有影响。


啊..你向我解释了这个谜团。清除线程变量是导致追加时复制的原因,多么简单但不明显。我的第二个问题是可变性问题的一个变体,涉及存储在StringReader中的引用。 - Erik Funkenbusch

3

newStringnewNewString是不同的string实例: newString是"Yo Ho Ho",而newNewString是"Yo Ho Ho and a bottle of rum."。 strings是不可变的,在调用StringBuilder.ToString()方法时,该方法返回表示当前状态的不可变string

最后一个语句将返回null或"Bar"?如果它返回其中之一,这是否是定义良好的行为?换句话说,我能依赖它吗?

它将返回nullStringReader正在处理您在构造函数中传递给它的不可变string,因此它不会受到您对StringBuilder所做的任何更改的影响。


2
这个类的整个目的是使字符串可变,确实如此。我认为(但不确定)只有在没有对此对象进行其他操作时,它才会返回相同的字符串。因此,在此之后,请注意:
String s_old = "Foo";
StringBuilder sb = new StringBuilder(s_old);
String s_new = sb.ToString();

s_old 和 s_new 是相同的,但不会出现在任何其他情况中。

需要注意的是,在 Java 编译器中,多个字符串拼接会自动转换为 StringBuilder(或类似但更快的 StringBuffer)类的操作。如果 .NET 编译器不进行此转换,我会非常惊讶。


C#编译器在字符串拼接时不使用StringBuilder,而是使用String.Concat。这意味着在任何拼接操作之前就已知最终长度。 - Jon Skeet

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