【问题标题】:Does string concatenation use StringBuilder internally?字符串连接是否在内部使用 StringBuilder?
【发布时间】:2011-02-22 01:07:23
【问题描述】:

我的三位同事刚刚告诉我,没有理由使用 StringBuilder 来代替使用 + 运算符的连接。换句话说,这可以处理一堆字符串:myString1 + myString2 + myString3 + myString4 + mySt...

他们使用的理由是,从 .NET 2 开始,如果您使用 + 运算符,C# 编译器将构建相同的 IL,就像您使用 StringBuilder 一样。

这对我来说是个新闻。他们是对的吗?

【问题讨论】:

    标签: c# compiler-construction string concatenation


    【解决方案1】:

    不,它们不正确。字符串连接会创建一个新的string,而StringBuilder 使用可变大小的缓冲区来构建字符串,只有在调用ToString() 时才会创建string 对象。

    如果您想进一步了解该主题,互联网上有很多关于字符串连接技术的讨论。大多数人关注在循环中使用不同方法的效率。在这种情况下,StringBuilder 比使用concatenations of 10 or more strings 的字符串运算符进行字符串连接更快,这应该表明它必须使用与连接不同的方法。

    也就是说,如果您要连接常量字符串值,则字符串运算符会更好,因为编译器会将它们分解,如果您执行非循环连接,则使用运算符会更好,因为它们应该会产生单次致电string.Concat。

    【讨论】:

    • 字符串连接不为每个+操作创建一个新字符串:a + b + c + d被转换为string.Concat(a, b, c, d)
    • @dtdb 但是如果你有一个像for(i=0;i<n;++i) s = s +a[i];这样的循环。
    • @Doc,不过,这不是 OP 所要求的。
    • 已编辑以涵盖循环中的连接与非循环中的连接之间的差异
    【解决方案2】:

    不,它们不正确,它不会产生相同的 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 的 IL:

    .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 
    }
    

    您也可能会觉得this article 很有趣。

    【讨论】:

    • IL 显然不同,但我相信问题是String.Concat 在内部做什么?是否使用StringBuilder?如果是这样,那么对使用 StringBuilder 并返回字符串的函数的调用与使用对 Concat 的单个调用并返回字符串的调用没有什么不同。当开始对 Concat 进行 多个 调用时,就会出现差异。还是我说的不对?
    • String.Concat 预先知道要连接的字符串的长度,因此与 StringBuilder 不同的是,它可以立即分配具有正确大小的新字符串,而无需分配不断增长的缓冲区和修剪结果。
    • Dmitrov:对——检查 IL,你比我强!但是,如果您使用带有“+”运算符的静态字符串,编译器将在不调用 concat 的情况下将它们组合起来。
    • 如果您事先知道长度,您可以将它们的总和作为容量传递给 StringBuilder 构造函数,它不必增加缓冲区或修剪结果。一个团队编写两次相同的代码而不是重用实现是很奇怪的,但并非闻所未闻。
    【解决方案3】:

    不,他们不是。它们肯定会产生不同的 IL。它使用不同的调用:String.Concat 在非 StringBuilder 情况下。

    String.Concat 调用一个名为ConcatArray 的私有方法,该方法分配一个新字符串,其长度刚好足以容纳最终结果。所以,非常不同,但这并不意味着使用 + 运算符进行连接比使用 StringBuilder 效率低。事实上,它几乎可以肯定更有效。此外,在连接常量的情况下,它是在编译时完成的。

    但是,当您在循环中进行连接时,编译器无法进行这种优化。在这种情况下,使用StringBuilder 对于相当长的字符串会更好。

    【讨论】:

      【解决方案4】:

      答案是取决于你如何连接。如果您将 + 运算符与静态字符串一起使用,那么您的朋友是正确的——不需要字符串生成器。但是,如果您使用字符串变量或 += 运算符,那么您正在重新分配字符串。

      真正了解这里发生了什么的方法是编写一些代码然后反编译它。

      让我们构建一些测试代码并在 Reflector 中使用 IL 视图查看它(或者您可以使用 ILDASM,无论您喜欢哪种方式

      首先,一个基线——这个方法根本不连接:

      static void NoConcat() { string test = "Hello World"; }

      现在是 IL:

      .method private hidebysig static void NoConcat() cil managed { .maxstack 1 .locals init ( [0] string test) L_0000: nop L_0001: ldstr "Hello World" <----------NO reallocation! L_0006: stloc.0 L_0007: ret }

      好吧,没什么意外吧?

      现在让我们看一些肯定会重新分配字符串的代码,所以我们知道它是什么样子的:

      static void Concat2() { string test = "Hello"; test += " "; test += "World"; }

      这里是 IL,注意重新分配(它调用 string.Concat,这会导致分配一个新字符串):

      .method private hidebysig static void Concat2() cil managed { .maxstack 2 .locals init ( [0] string test) L_0000: nop L_0001: ldstr "Hello" L_0006: stloc.0 L_0007: ldloc.0 L_0008: ldstr " " L_000d: call string [mscorlib]System.String::Concat(string, string) L_0012: stloc.0 L_0013: ldloc.0 L_0014: ldstr "World" L_0019: call string [mscorlib]System.String::Concat(string, string) L_001e: stloc.0 L_001f: ret }

      好的,现在如何连接不会导致重新分配 - 我们将使用“+”运算符连接静态字符串:

      static void Concat1() { string test = "Hello" + " " + "World"; }

      这是 IL —— 看看编译器有多聪明!它不使用 concat - 它与第一个示例相同:

      .method private hidebysig static void Concat1() cil managed { .maxstack 1 .locals init ( [0] string test) L_0000: nop L_0001: ldstr "Hello World" L_0006: stloc.0 L_0007: ret }

      现在让我们玩得开心一点。如果我们混合静态字符串和变量怎么办? (这是你最好还是使用字符串生成器的地方)

      static void Concat3(string text) { string test = "Hello" + " " + text + " World"; }

      还有 IL。请注意,将“Hello”和“”组合为常量已经足够聪明了,但它仍然需要对 text 变量进行 concat:

      .method private hidebysig static void Concat3(string text) cil managed { .maxstack 3 .locals init ( [0] string test) 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 }

      【讨论】:

        【解决方案5】:

        我通常遵循以下规则:

        1. 如果子字符串的数量是已知的,请使用串联。这涵盖了 str1 + str2 + str3 +... 这样的情况,无论它们有多少。

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

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

        【讨论】:

        • 使用String.Concat 而不是String.Join :)
        • @Thorin String.Join 是所有 .NET 字符串组合函数中性能最高的,您可以四处搜索以找到显示这一点的基准测试结果。但是,通常 string.Join 使用起来并不友好。
        • @Chris:好的,基准测试似乎证实了这一点。那是 String.Concat 实现中的一些 EPIC 失败。
        • string.concat 内部使用 string.join
        【解决方案6】:

        String 和 StringBuilder 的区别在于:

        连接一个字符串将创建一个新的字符串对象作为连接的结果。连接 StringBuilder 会修改字符串对象。

        所以他们不正确。

        【讨论】:

          【解决方案7】:

          字符串连接和 StringBuidler 之间存在巨大的性能差异。我们的网络服务太慢了。我们将所有的字符串猫都改成了 StringBuilder.Appends,它变得更快了!

          【讨论】:

          • 这让我想知道为什么你首先要做这么多的字符串连接......
          【解决方案8】:

          不,字符串连接在内部不使用 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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2023-03-08
            • 2011-07-22
            • 1970-01-01
            • 2014-04-06
            • 1970-01-01
            • 2011-12-26
            • 1970-01-01
            • 2012-07-06
            相关资源
            最近更新 更多