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

我的三个同事告诉我,没有理由使用StringBuilder代替使用+运算符进行连接。 换句话说,这对一堆字符串很好: myString1 + myString2 + myString3 + myString4 + mySt...

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

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

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

如果您想进一步阅读有关该主题的内容,那么在互联网上有很多关于字符串连接技术的讨论。 在循环中使用时,大多数都关注不同方法的效率。 在这种情况下, StringBuilder使用字符串运算符进行字符串连接比10个或更多字符串的连接更快,这应该表明它必须使用与连接不同的方法。

也就是说,如果你连接常量字符串值,字符串运算符会更好,因为编译器会将它们分解,如果执行非循环连接,使用运算符会更好,因为它们应该导致单个调用string.Concat

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

IL的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 } 

IL of Concat:

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

你也可能会觉得这篇文章很有意思。

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

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

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

答案是它取决于你如何连接。 如果你使用带有静态字符串的+运算符,那么你的朋友是正确的 – 不需要字符串构建器。 但是,如果使用字符串变量或+ =运算符,则需要重新分配字符串。

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

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

首先,一个基线 – 这个方法根本没有连接:

static void NoConcat() { string test = "Hello World"; }
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 }
.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"; }
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 }
.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"; }
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 }
.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 } 

现在让我们玩得开心吧。 如果我们混合静态字符串和变量怎么办? (这是你使用stringbuilder可能还会更好的地方)

static void Concat3(string text) { string test = "Hello" + " " + text + " World"; }
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 }
.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 } 

我通常遵循以下规则:

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

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

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

String和StringBuilder之间略有不同:

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

所以他们不正确。

字符串连接和StringBuidler之间存在巨大的性能差异。 我们的网络服务太慢了。 我们将所有的串猫改为StringBuilder.Appends并且速度更快了!

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