StringBuilder限制

3

我读到了StringBuilder类型有一个限制(默认为16个字符),当你在它上面添加一些文本超过了它的限制时,会创建一个新的实例并将数据复制到其中。我使用以下代码尝试了一下:

StringBuilder test = new StringBuilder("ABCDEFGHIJKLMNOP",16);
test.Append("ABC");

生成的 CIL 代码如下:

  .maxstack  3
  .locals init (class [mscorlib]System.Text.StringBuilder V_0)
  IL_0000:  nop
  IL_0001:  ldstr      "ABCDEFGHIJKLMNOP"
  IL_0006:  ldc.i4.s   16
  IL_0008:  newobj     instance void [mscorlib]System.Text.StringBuilder::.ctor(string, int32)
  IL_000d:  stloc.0
  IL_000e:  ldloc.0
  IL_000f:  ldstr      "ABC"
  IL_0014:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0019:  pop
  IL_001a:  ret

将限制设置为32:

StringBuilder test = new StringBuilder("ABCDEFGHIJKLMNOP",32);
test.Append("ABC");

生成了完全相同的IL代码。 我期望的是在第一种情况下创建一个新实例,在第二种情况下更改实例的值,但显然这并没有发生,有什么线索吗?

4个回答

8

所有有趣的事情都发生在这一行:

IL_0014:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)

这里是调用Append()方法的位置,但你所发布的IL代码并不包含该方法的主体。请查看StringBuilder类的源代码(它是在允许你查看的许可证下发布的),并查看Append()方法内部发生了什么。 剧透警告!查看Append()的源代码将揭示:每当连接字符串的长度超过当前缓冲区的大小时,内部缓冲区确实会增加。

但生成的 CIL 代码表明它并不是这样,据我理解,每当我向其附加文本时,就会创建一个新的字符串生成器,就像你操作字符串时创建新字符串一样。 - Moayad Mardini
不确定您的意思 - 您在IL中寻找什么?我只看到一个对构造函数(IL_0008)和一个对Append()方法(IL_0008)的调用。这似乎很好地反映了您的C#代码。 - Jørn Schou-Rode
1
不,不会创建一个新的StringBuilder对象。现有的StringBuilder分配一个新的缓冲区。 - Joel Coehoorn
1
您不会获得一个新的 StringBuilder 实例。重新分配发生在 StringBuilder 内部,不会影响调用代码。 - Per Erik Stendahl
在我的理解中,OP的观点是无论他使用32还是16作为参数,StringBuilder都是使用相同的缓冲区创建的。 - hangy
如果hangy的解释是正确的:请查看StringBuilder构造函数的源代码。当调用内部string.GetStringForStringBuilder()方法时,它确实使用提供的长度。 - Jørn Schou-Rode

3
这段C#代码
using System.Text;

internal class Program
{
    internal static void Main(string[] args)
    {
        StringBuilder test = new StringBuilder("ABCDEFGHIJKLMNOP", 16);
        test.Append("ABC");

        StringBuilder test2 = new StringBuilder("ABCDEFGHIJKLMNOP", 32);
        test2.Append("ABC");
    }
}

根据Reflector,它生成以下IL:

.class private auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0 
        L_0001: call instance void [mscorlib]System.Object::.ctor()
        L_0006: ret 
    }

    .method assembly hidebysig static void Main(string[] args) cil managed
    {
        .entrypoint
        .maxstack 3
        .locals init (
            [0] class [mscorlib]System.Text.StringBuilder test,
            [1] class [mscorlib]System.Text.StringBuilder test2)
        L_0000: nop 
        L_0001: ldstr "ABCDEFGHIJKLMNOP"
        L_0006: ldc.i4.s 0x10
        L_0008: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor(string, int32)
        L_000d: stloc.0 
        L_000e: ldloc.0 
        L_000f: ldstr "ABC"
        L_0014: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
        L_0019: pop 
        L_001a: ldstr "ABCDEFGHIJKLMNOP"
        L_001f: ldc.i4.s 0x20
        L_0021: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor(string, int32)
        L_0026: stloc.1 
        L_0027: ldloc.1 
        L_0028: ldstr "ABC"
        L_002d: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
        L_0032: pop 
        L_0033: ret 
    }
}

因此,在这里,0x100x20被用于初始化testtest2,这意味着您可能在测试中查看了错误的IL代码?


谢谢!“ldc.i4.s 0x10/0x20”是什么意思? - Moayad Mardini
0x20(十六进制)= 32 dez,0x10(十六进制)= 16 dez :) 我不太熟悉IL,但我认为这是以十六进制值设置的构造函数参数。 - hangy

2

StringBuilder类有一个Capacity属性,就像大多数集合类一样。当集合的大小超过容量时,内部数据结构(而不是对象本身)会被重新分配,我认为大多数集合类(我确定StringBuilder和List< >)使用加倍策略。是的,这涉及创建一个新数组并复制旧数据。

如果您有关于最终结果大小的任何信息,请使用以下内容:

var sb = new StringBuilder(n); // set initial Capacity=n

高估一些并增加额外的空间并不是个坏主意。最好分配多几个字符,而不是在中途被复制。

这就是我们为了拥有可以自动增长的集合(StringBuilder类似于一个字符集合)而付出的代价。我猜,其他替代方案,比如块链表,被认为太过复杂。


请进一步说明“内部数据结构(而不是对象本身)如何重新分配”,谢谢。 - Moayad Mardini
StringBuilder需要一些内部存储来保存文本,我猜它是像char[] text = new char[Capacity]这样的东西。如果重新创建实际的StringBuilder,它将无法更新对对象的所有引用,这将造成严重破坏。 - H H
1
想象一个类有一个保存数组的类级别变量。你能否替换存储在此变量中的数组,而不改变类的实例?当然可以! - Joel Mueller
Joel,你的例子很好地说明了这个问题,非常感谢 :) - Moayad Mardini

1

我认为你误读了IL代码。以下是该行:

 IL_0014:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)

这并不意味着创建了一个新的StringBuilder实例;它只是对Append方法的调用。

如果调用Append会导致字符串长度超过当前StringBuilder的容量,它将在内部创建一个新的String实例;但仍然是同一个StringBuilder实例。


我对新字符串实例是如何在内部创建非常好奇,事实上,这正是我想要了解的,谢谢! - Moayad Mardini

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