【问题标题】:How does StringBuilder work internally in C#?StringBuilder 如何在 C# 内部工作?
【发布时间】:2011-06-29 16:44:18
【问题描述】:

StringBuilder 是如何工作的?

它在内部做什么?它使用不安全的代码吗? 为什么这么快(与+ 运算符相比)?

【问题讨论】:

标签: c# .net string stringbuilder


【解决方案1】:

当您使用 + 运算符构建字符串时:

string s = "01";
s += "02";
s += "03";
s += "04";

然后在第一次连接时,我们创建一个长度为 4 的新字符串,并将“01”和“02”复制到其中——复制了四个字符。在第二个连接中,我们创建了一个长度为 6 的新字符串,并将“0102”和“03”复制到其中——复制了六个字符。在第三个 concat 中,我们创建一个长度为 8 的字符串并将“010203”和“04”复制到其中——复制了八个字符。到目前为止,这个八字符的字符串总共复制了 4 + 6 + 8 = 18 个字符。继续。

...
s += "99";

在第 98 个 concat 中,我们创建一个长度为 198 的字符串,并将“010203...98”和“99”复制到其中。这给了我们总共 4 + 6 + 8 + ... + 198 = 很多,以制作这个 198 个字符的字符串。

字符串生成器不会进行所有的复制。相反,它维护一个希望大于最终字符串的可变数组,并在必要时将新内容填充到数组中。

当猜测错误并且数组已满时会发生什么?有两种策略。在之前版本的框架中,字符串生成器会在数组满时重新分配和复制数组,并将其大小翻倍。在新的实现中,字符串生成器维护一个相对较小的数组的链表,并在旧数组满时将一个新数组附加到列表的末尾。

此外,正如您猜想的那样,字符串生成器可以使用“不安全”代码来提高其性能。例如,将新数据写入数组的代码已经检查过数组写入是否在界限内。通过关闭安全系统,它可以避免每次写入检查,否则抖动可能会插入以验证对阵列的每次写入都是安全的。字符串构建器执行了许多此类技巧来执行诸如确保缓冲区被重用而不是重新分配、确保避免不必要的安全检查等事情。我建议不要使用这些恶作剧,除非你真的很擅长正确编写不安全的代码,并且确实需要勉强发挥最后一点性能。

【讨论】:

  • 可能值得添加一个注释,即如果您执行 string s = x + y + z; 则不会发生这种情况; (使用 String.Concat)以防任何愚蠢的人决定他们需要将所有这些“优化”到 StringBuilders 中(说真的,我认识这样做的人)
  • 我不知道新版本的 StringBuilder - 这是一个很好的优化。如果我将一个大字符串附加到字符串构建器,它是使用您为新创建的数组提到​​的“相对较小”的数组大小,还是使用更大的大小来适应我的整个新字符串?
  • 没关系,我自己找到了答案 - 字符串生成器通常会根据需要扩展,或者将其大小加倍 - 以较大者为准。
  • 如果你做了string s = "01" + "02" + "03" + "04",它会编译成string s = string.Concat("01","02","03","04")吗? (实际上我认为编译器只会将其优化为string s = "01020304",但如果所有部分都不是硬编码的字符串值,它会使用 String.Concat 吗?)
  • @Nick:是的。对 Concat 的每个 call 都会获取其所有参数的 total 长度,并分配一个足够大的新字符串。
【解决方案2】:

StringBuilder 的实现在版本之间发生了变化,我相信。但从根本上说,它保持了某种形式的可变结构。我相信它使用来使用仍在变异的字符串(使用内部方法),并且只是确保它在返回后永远不会变异。

StringBuilder 比在循环中使用字符串连接更快的原因 正是因为可变性 - 它不需要在每次突变后构造新字符串,这意味着复制字符串等中的所有数据。

对于单个串联,使用+ 实际上比使用StringBuilder 更有效。只有当您执行多个操作并且您并不真正需要StringBuilder 发光的中间结果时。

更多信息请参见my article on StringBuilder

【讨论】:

【解决方案3】:

Microsoft CLR 确实通过内部调用执行了一些操作(与不安全代码不太一样)。与一堆 + 连接的字符串相比,最大的性能优势是它写入 char[] 并且不会创建尽可能多的中间字符串。当您调用 ToString() 时,它会根据您的内容构建一个完整的、不可变的字符串。

【讨论】:

  • 您介意提供更多详细信息吗?当您将项目组合在一起并创建一个巨大的字符串时,它是否会重新定义数组大小?它只是一个指向 char 数组(或链表)的指针数组,在调用 tostring 时变成了一个对象?您介意引用来源吗?
  • 内部是透明的,但由于它有像 StringBuilder.EnsureCapacity 这样的方法,它让人相信它是一个大缓冲区,必要时会增长。
  • 这不会比使用链表并在最后合并它效率低吗?我的意思是,如果您要附加一个 1 兆的字符串怎么办?您必须创建一个已经存在的东西的副本,这需要时间和资源。如果你只是做了一个指向原始的指针,你就不必担心它会改变,因为它是不可变的,而且 gac 不会因为你引用不可变字符串而触摸它来删除它。
  • @JSWork,如果我说stringBuilder.Remove(1023, 2000)。如果您有一个字符串链接列表,那将是一个复杂的算法。我敢肯定那不会那么有效。但是,如果您知道不需要插入、删除、替换等功能,请随意实现您自己的 LLStringBuilder 类。
  • @JSWork,使用new StringBuilder(2048*1024),您可以指定足够大的初始容量,这将最大限度地降低重新调整的成本。 (您可能已经知道这一点,但它可能会使未来的读者受益。)
【解决方案4】:

StringBuilder 使用可以更改的字符串缓冲区,而常规的String 则不能。当您调用StringBuilderToString 方法时,它只会冻结字符串缓冲区并将其转换为常规字符串,因此不必额外复制所有数据。

由于StringBuilder 可以更改字符串缓冲区,它不必为字符串数据的每次更改都创建一个新的字符串值。当您使用 + 运算符时,编译器会将其转换为创建新字符串对象的 String.Concat 调用。这段看似无辜的代码:

str += ",";

编译成这样:

str = String.Concat(str, ",");

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-22
    • 1970-01-01
    • 2020-06-13
    • 1970-01-01
    • 1970-01-01
    • 2022-10-03
    相关资源
    最近更新 更多