字符串拼接内部是否使用StringBuilder?

12

我的三位同事告诉我,在使用 + 运算符拼接字符串时,没有必要使用 StringBuilder。换句话说,这样做是可以的:myString1 + myString2 + myString3 + myString4 + mySt...

他们使用的理由是自 .NET 2 以来,C# 编译器将生成与使用 StringBuilder 相同的 IL。

这对我来说是新闻。他们正确吗?

8个回答

25
不,他们是不正确的。字符串连接会创建一个新的string对象,而StringBuilder使用可变大小的缓冲区来构建字符串,在调用ToString()时才会创建一个string对象。
如果您想进一步了解字符串连接技术,互联网上有很多讨论。大多数人都关注在循环中使用不同方法的效率。在这种情况下,当使用字符串运算符进行10个或更多字符串的连接时,StringBuilder比字符串连接更快,这表明它必须使用不同的方法进行连接。
尽管如此,如果您正在连接常量字符串值,则字符串运算符会更好,因为编译器会将它们合并,并且如果您执行非循环连接,则使用运算符也会更好,因为它们应该会导致单个调用string.Concat

10
字符串拼接并不会为每个+操作创建一个新的字符串:将a + b + c + d 转换为 string.Concat(a, b, c, d) - dtb
1
@dtdb 如果你有一个像 for(i=0;i<n;++i) s = s +a[i]; 这样的循环,它就会执行。 - Doc Brown
2
@Doc,不过那不是原帖所问的。 - Joey
1
编辑以涵盖循环中和非循环中连接的区别 - Jeff Yates

16

不,它们是不正确的,这不会产生相同的IL:

static string StringBuilder()
{
    var s1 = "s1";
    var s2 = "s2";
    var s3 = "s3";
    var s4 = "s4";
    var sb = new StringBuilder();
    sb.Append(s1).Append(s2).Append(s3).Append(s4);
    return sb.ToString();
}

static string Concat()
{
    var s1 = "s1";
    var s2 = "s2";
    var s3 = "s3";
    var s4 = "s4";
    return s1 + s2 + s3 + s4;
}

StringBuilder的内部语言:

.method private hidebysig static string StringBuilder() cil managed
{
    .maxstack 2
    .locals init (
        [0] string s1,
        [1] string s2,
        [2] string s3,
        [3] string s4,
        [4] class [mscorlib]System.Text.StringBuilder sb)
    L_0000: ldstr "s1"
    L_0005: stloc.0 
    L_0006: ldstr "s2"
    L_000b: stloc.1 
    L_000c: ldstr "s3"
    L_0011: stloc.2 
    L_0012: ldstr "s4"
    L_0017: stloc.3 
    L_0018: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
    L_001d: stloc.s sb
    L_001f: ldloc.s sb
    L_0021: ldloc.0 
    L_0022: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
    L_0027: ldloc.1 
    L_0028: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
    L_002d: ldloc.2 
    L_002e: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
    L_0033: ldloc.3 
    L_0034: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
    L_0039: pop 
    L_003a: ldloc.s sb
    L_003c: callvirt instance string [mscorlib]System.Object::ToString()
    L_0041: ret 
}

Concat的IL代码:

.method private hidebysig static string Concat() cil managed
{
    .maxstack 4
    .locals init (
        [0] string s1,
        [1] string s2,
        [2] string s3,
        [3] string s4)
    L_0000: ldstr "s1"
    L_0005: stloc.0 
    L_0006: ldstr "s2"
    L_000b: stloc.1 
    L_000c: ldstr "s3"
    L_0011: stloc.2 
    L_0012: ldstr "s4"
    L_0017: stloc.3 
    L_0018: ldloc.0 
    L_0019: ldloc.1 
    L_001a: ldloc.2 
    L_001b: ldloc.3 
    L_001c: call string [mscorlib]System.String::Concat(string, string, string, string)
    L_0021: ret 
}

你可能会发现这篇文章很有趣。


1
IL 显然是不同的,但我相信问题是 String.Concat 在内部做了什么?它是否使用了 StringBuilder?如果是这样,那么调用一个使用 StringBuilder 并返回字符串的函数与使用单个 Concat 调用并返回字符串的调用没有区别。当开始进行 多个 调用时,差异就会出现。或者我错了吗? - Anthony Pegram
7
String.Concat 在串联字符串时提前知道字符串长度,因此与 StringBuilder 不同,它可以立即分配正确大小的新字符串,而无需分配一个增长缓冲区并修剪结果。 - dtb
Dmitrov:没错——检查IL,你比我快!但是,如果您使用带有“+”运算符的静态字符串,则编译器将在不调用concat的情况下将它们组合。 - JMarsch
如果您事先知道长度,可以将它们的总和作为容量传递给StringBuilder构造函数,这样它就不必增加其缓冲区或修剪结果。一个团队编写两次相同的代码而不是重用实现可能有些奇怪,但并非闻所未闻。 - Pete Kirkham

5
不,它们不一样。它们生成不同的IL代码。在非StringBuilder情况下,使用了不同的调用:String.ConcatString.Concat 调用一个名为ConcatArray的私有方法,该方法分配一个新字符串,其长度恰好足以容纳最终结果。因此,这两种方法非常不同,但这并不意味着使用+运算符连接字符串比使用StringBuilder效率低。事实上,前者几乎肯定更高效。此外,在常量连接的情况下,连接操作是在编译时完成的。
然而,当您在循环中进行连接操作时,编译器无法执行此类优化。在这种情况下,对于相对较长的字符串,使用StringBuilder会更好。

4
答案取决于你如何拼接字符串。如果你使用静态字符串与 + 操作符相连,那么你的朋友是正确的——不需要使用字符串构建器。然而,如果你使用字符串变量或 += 操作符,则会重新分配字符串。
要真正了解发生了什么,请编写一些代码,然后反编译它。
让我们构建一些测试代码,并在 Reflector 中查看 IL 视图(或者您可以使用 ILDASM,无论哪个都可以)。
首先,一个基准——这个方法根本不连接:
现在是 IL:
好的,没有惊喜,对吧?
现在让我们看一些明确重新分配字符串的代码,以便知道它的样子:
这里是 IL,请注意重新分配(它调用 string.Concat,导致新字符串被分配):
好的,现在怎么样才能不导致重新分配连接——我们将使用“+”运算符连接静态字符串:
这里是 IL——看看编译器多聪明!它不使用 Concat——它与第一个示例相同:

.方法 私有 hidebysig 静态 空的 Concat1() cil 管理
{
    .最大堆栈 1
    .本地变量 初始化 (
        [0] 字符串 测试)
    L_0000: nop 
    L_0001: ldstr "你好世界"
    L_0006: stloc.0 
    L_0007: ret 
}
现在让我们来玩一点。 如果我们混合使用静态字符串和变量会怎样?(这时您可能最好使用stringbuilder)
静态 void Concat3(string text) { string test = "Hello" + " " + text + " World"; }
IL如下所示。 请注意,它足够聪明,将“Hello”和“ ”组合为一个常数,但仍必须为文本变量进行连接:
.方法 私有 hidebysig 静态 空的 Concat3(string text) cil 管理 { .最大堆栈 3 .本地变量 初始化 ( [0] 字符串 测试) L_0000: nop L_0001: ldstr "Hello " L_0006: ldarg.0 L_0007: ldstr " World" L_000c: call string [mscorlib]System.String::Concat(string, string, string) L_0011: stloc.0 L_0012: ret }

1

我通常遵循以下规则:

  1. 如果子字符串的数量是已知的,请使用连接。这适用于像str1 + str2 + str3 + ...这样的情况,无论它们有多少个。

  2. 如果子字符串已经在数组中,请使用string.join

  3. 如果在循环中构建字符串,请使用StringBuilder


使用 String.Concat 替代 String.Join :) - Thorarin
1
@Thorain String.Join是所有.NET字符串组合函数中最高效的,您可以在谷歌上查找基准测试结果来证明这一点。然而,通常情况下,string.Join并不易于使用。 - Chris Marisic
1
@Chris:好的,基准测试似乎证实了这一点。那么String.Concat实现中就出现了一些史诗级别的失败。 - Thorarin
string.concat 内部使用 string.join。 - Codism

0
不,字符串连接并不在内部使用 StringBuilder。然而,在你的特定示例中,使用 StringBuilder 没有任何优势。
对于少量字符串来说,这很好(你只创建了一个新字符串):
myString = myString + myString2 + myString3 + myString4 + mySt...

这不是(你正在创建和分配4个字符串等):

myString = myString + myString2;
myString = myString + myString3;
myString = myString + myString4;
myString = myString + myString5;

在所有关于这个问题的stackoverflow问题中,这个答案是最好的之一: String vs. StringBuilder

找到两个答案,一个是Jay Bazuzi写的,另一个是James Curran写的。

另外,强烈推荐Jeff Atwood使用实际测试来比较字符串拼接/构建的其他情况,链接如下: http://www.codinghorror.com/blog/2009/01/the-sad-tragedy-of-micro-optimization-theater.html


0

String和StringBuilder之间存在一些细微的差别:

连接一个String将创建一个新的字符串对象,该对象是连接的结果。连接一个StringBuilder会修改字符串对象。

因此它们并不正确。


0

字符串拼接和StringBuilder之间有巨大的性能差异。我们有一个速度太慢的Web服务。我们将所有的字符串拼接改为StringBuilder.Append,速度就快了很多!


这让我想知道为什么你一开始要做那么多的字符串拼接... - Thorarin

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