众所周知,在.NET中String是引用类型,具有不可变性,当一个String对象被修改、插入、连接、截断时,新的String对象就将被分配,这会直接影响到性能。但在实际开发中经常碰到的情况是,一个String对象的最终生成需要经过一个组装的过程,而在这个组装过程中必将会产生很多临时的String对象,而这些String对象将会在堆上分配,需要GC来回收,这些动作都会对程序性能产生巨大的影响。事实上,在String的组装过程中,其临时产生的String对象实例都不是最终需要的,因此可以说是没有必要分配的。.
鉴于此,在.NET中提供了StringBuilder,其设计思想源于构造器(Builder)设计模式,致力于解决复杂对象的构造问题。对于String对象,正需要这样的构造器来进行组装。StringBuilder类型在最终生成String对象之前,将不会产生任何String对象,这很好地解决了字符串操作的性能问题。
以下代码展示了使用StringBuilder和不适用StringBuilder的性能差异:(这里的性能检测工具使用了老赵的CodeTimer类)
public class Program
{
private const String item = "一个项目";
private const String split = ";";
static void Main(string[] args)
{
int number = 10000;
// 使用StringBuilder
CodeTimer.Time("使用StringBuilder: ", 1, () =>
{
UseStringBuilder(number);
});
// 不使用StringBuilder
CodeTimer.Time("使用不使用StringBuilder: : ", 1, () =>
{
NotUseStringBuilder(number);
});
Console.ReadKey();
}
static String UseStringBuilder(int number)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < number; i++)
{
sb.Append(item);
sb.Append(split);
}
sb.Remove(sb.Length - 1, 1);
return sb.ToString();
}
static String NotUseStringBuilder(int number)
{
String result = "";
for (int i = 0; i < number; i++)
{
result += item;
result += split;
}
return result;
}
}
上述代码的运行结果如下图所示,可以看出由于StringBuilder不会产生任何的中间字符串变量,因此效率上优秀不少!
看到StringBuilder这么优秀,不禁想发出一句:卧槽,牛逼!
于是,我们拿起我们的锤子(Reflector)撕碎StringBuilder的外套,看看里面到底装了什么?
我们发现,在StringBuilder中定义了一个字符数组m_ChunkChars,它保存StringBuilder所管理着的字符串中的字符。
经过对StringBuilder默认构造方法的分析,系统默认初始化m_ChunkChars的长度为16(0x10),当新追加进来的字符串长度与旧有字符串长度之和大于该字符数组容量时,新创建字符数组的容量会增加到2n+1(假如当前字符数组容量为2n)。
此外,StringBuilder内部还有一个同为StringBuilder类型的m_ChunkPrevious,它是内部的一个StringBuilder对象,前面提到当追加的字符串长度和旧字符串长度之合大于字符数组m_ChunkChars的最大容量时,会根据当前的(this)StringBuilder创建一个新的StringBuilder对象,将m_ChunkPrevious指向新创建的StringBuilder对象。
下面是StringBuilder中实现扩容的核心代码:
private void ExpandByABlock(int minBlockCharCount)
{
......
int num = Math.Max(minBlockCharCount, Math.Min(this.Length, 0x1f40));
this.m_ChunkPrevious = new StringBuilder(this);
this.m_ChunkOffset += this.m_ChunkLength;
this.m_ChunkLength = 0;
......
this.m_ChunkChars = new char[num];
}
可以看出,初始化m_ChunkPrevious在前,创建新的字符数组m_ChunkChars在后,最后才是复制字符到数组m_ChunkChars中(更新当前的m_ChunkChars)。归根结底,StringBuilder是在内部以字符数组m_ChunkChars为基础维护一个链表m_ChunkPrevious,该链表如下图所示:
在最终的ToString方法中,当前的StringBuilder对象会根据这个链表以及记录的长度和偏移变量去生成最终的一个String对象实例,StringBuilder的内部实现中使用了一些指针操作,其内部原理有兴趣的园友可以自己去通过反编译工具查看源代码。