StringBuilder研究和探索
由于String类型代表的是一个不可变的字符串,所以BCL提供了另一个名为System.Text.StringBuilder的类型,它允许我们有效的对字符串的字符执行动态操作,以创建一个String。
从逻辑上说,StringBuilder对象中包含一个字段,它引用由Char结构构成的一个数组。StringBuilder的成员允许我们操作这个字符,有效的缩减字符串的大小或者更改字符串中的字符。如果字符串变大,超过已经分配的字符的大小,StringBuilder就会自动的分配一个全新的、更大的数组,并开始使用新的数组,前一个数组会被垃圾回收器回收。用StringBuilder对象构建好字符串之后,为了将StringBuilder的字符“转换”成一个String,只需调用StringBuilder的ToString方法,在内部,该方法只是返回对StringBuilder内部维护的字符串的字段的一个引用,执行效率非常快,因为它不需要进行字符数组复制。
StringBuilder既是具体建造者(Builder)又是指导者(Director),最终生成一个复杂的String对象作为产品(Product)。在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略掉指导者角色。让Builder角色自己扮演指导者与建造者双重角色。
private const string CapacityField = "Capacity";
internal const int DefaultCapacity = 0x10;
internal IntPtr m_currentThread;
internal int m_MaxCapacity;
internal volatile string m_StringValue;
private const string MaxCapacityField = "m_MaxCapacity";
private const string StringValueField = "m_StringValue";
private const string ThreadIDField = "m_currentThread";
上面是StringBuilder的一些Fields,其中主要的字段是MaxCapacity和StringValue。 注意StringValue的定义有个Volatile。
C#中volatile 关键字指示一个字段可以由多个同时执行的线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。这样可以确保该字段在任何时间呈现的都是最新的值。可变关键字仅可应用于类或结构字段。不能将局部变量声明为 volatile。
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。
用volatile关键字声明的变量i每一次被访问时,执行部件都会从i相应的内存单元中取出i的值。
没有用volatile关键字声明的变量i在被访问的时候可能直接从cpu的寄存器中取值(因为之前i被访问过,也就是说之前就从内存中取出i的值保存到某个寄存器中),之所以直接从寄存器中取值,而不去内存中取值,是因为编译器优化代码的结果(访问cpu寄存器比访问ram快的多)。
以上两种情况的区别在于被编译成汇编代码之后,两者是不一样的。之所以这样做是因为变量i可能会经常变化,保证对特殊地址的稳定访问。
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
我们在使用StringBuilder的时候,先new一个实例,看看构造函数
{
}
public StringBuilder(int capacity) : this(string.Empty, capacity)
{
}
public StringBuilder(string value) : this(value, 0x10)
{
}
public StringBuilder(int capacity, int maxCapacity)
{
this.m_currentThread = Thread.InternalGetCurrentThread(); //获取线程ID
if (capacity > maxCapacity)
{
throw new ArgumentOutOfRangeException("capacity", Environment.GetResourceString("ArgumentOutOfRange_Capacity"));
}
if (maxCapacity < 1)
{
throw new ArgumentOutOfRangeException("maxCapacity", Environment.GetResourceString("ArgumentOutOfRange_SmallMaxCapacity"));
}
if (capacity < 0)
{
throw new ArgumentOutOfRangeException("capacity", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBePositive"), new object[] { "capacity" }));
}
if (capacity == 0)
{
capacity = Math.Min(0x10, maxCapacity);
}
this.m_StringValue = string.GetStringForStringBuilder(string.Empty, capacity);
this.m_MaxCapacity = maxCapacity;
}
public StringBuilder(string value, int capacity) : this(value, 0, (value != null) ? value.Length : 0, capacity)
{
}
public StringBuilder(string value, int startIndex, int length, int capacity)
{
this.m_currentThread = Thread.InternalGetCurrentThread();
if (capacity < 0)
{
throw new ArgumentOutOfRangeException("capacity", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBePositive"), new object[] { "capacity" }));
}
if (length < 0)
{
throw new ArgumentOutOfRangeException("length", string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("ArgumentOutOfRange_MustBeNonNegNum"), new object[] { "length" }));
}
if (startIndex < 0)
{
throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex"));
}
if (value == null)
{
value = string.Empty;
}
if (startIndex > (value.Length - length))
{
throw new ArgumentOutOfRangeException("length", Environment.GetResourceString("ArgumentOutOfRange_IndexLength"));
}
this.m_MaxCapacity = 0x7fffffff;
if (capacity == 0)
{
capacity = 0x10;
}
while (capacity < length) //如果长度大于了容量,则扩容2倍
{
capacity *= 2;
if (capacity < 0)
{
capacity = length;
break;
}
}
this.m_StringValue = string.GetStringForStringBuilder(value, startIndex, length, capacity);
}
还有经常用到的Append方法:
{
if (value != null)
{
string stringValue = this.m_StringValue;
IntPtr currentThread = Thread.InternalGetCurrentThread();//获取线程ID
if (this.m_currentThread != currentThread)
{
stringValue = string.GetStringForStringBuilder(stringValue, stringValue.Capacity);//如果是不同线程,则值重新设置
}
int length = stringValue.Length;
int requiredLength = length + value.Length;
if (this.NeedsAllocation(stringValue, requiredLength))//看是否需要扩容
{
string newString = this.GetNewString(stringValue, requiredLength);
newString.AppendInPlace(value, length);//追加新值
this.ReplaceString(currentThread, newString);//把线程ID的字符串值更新
}
else
{
stringValue.AppendInPlace(value, length);
this.ReplaceString(currentThread, stringValue);
}
}
return this;
}
StringBuilder在需要多次修改字符串值的时候是个比较好的工具,他的性能比直接修改string要快很多,下面将通过IL代码来看看,实际运用中如果多次修改string产生的代价:
{
public class a
{
static void Main()
{
string s = "1 " + "2 " + "3 ";
Console.WriteLine(s);
}
}
}
//IL如下
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 13 (0xd)
.maxstack 1
.locals init (string V_0)
IL_0000: ldstr "123 "
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: call void [mscorlib]System.Console::WriteLine(string)
IL_000c: ret
} // end of method a::Main
在这里我们看到这么一句: ldstr "123 ",String 是引用类型,可它却没有newobj指令,这说明CLR构造String与其他的对象不一样。还有,不用我说你们都看到了:为什么是“123”,原因是编译器在编译时将它们合并成一个字符串放到了程序集的元数据中(仅限于文字字符)。这样,运行时CLR直接将它们取出来用就OK了,不用再“分别取出来,重新分配内存,复制数据“。
关于下面这个问题,结果却是不一样的:
{
public class a
{
static void Main()
{
string s1 = "1 ";
string s2 = "2 ";
string s3 = "3 ";
string s = s1 + s2 + s3;
Console.WriteLine(s);
}
}
}
//IL如下
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 34 (0x22)
.maxstack 3
.locals init (string V_0,
string V_1,
string V_2,
string V_3)
IL_0000: ldstr "1 "
IL_0005: stloc.0
IL_0006: ldstr "2 "
IL_000b: stloc.1
IL_000c: ldstr "3 "
IL_0011: stloc.2
IL_0012: ldloc.0
IL_0013: ldloc.1
IL_0014: ldloc.2
IL_0015: call string [mscorlib]System.String::Concat(string,
string,
string)
IL_001a: stloc.3
IL_001b: ldloc.3
IL_001c: call void [mscorlib]System.Console::WriteLine(string)
IL_0021: ret
} // end of method a::Main
这里我们关心的是这一句:System.String::Concat(string, string,string) 。这一句的源代码上面给出来了,它实现分配一块内存,然后将s1,s2,s3的内容复制过去。
再请看:
{
.entrypoint
// Code size 55 (0x37)
.maxstack 2
.locals init (string V_0,
string V_1,
string V_2,
string V_3)
IL_0000: ldstr "1 "
IL_0005: stloc.0
IL_0006: ldstr "2 "
IL_000b: stloc.1
IL_000c: ldstr "3 "
IL_0011: stloc.2
IL_0012: ldstr " "
IL_0017: stloc.3
IL_0018: ldloc.3
IL_0019: ldloc.0
IL_001a: call string [mscorlib]System.String::Concat(string,
string)
IL_001f: stloc.3
IL_0020: ldloc.3
IL_0021: ldloc.1
IL_0022: call string [mscorlib]System.String::Concat(string,
string)
IL_0027: stloc.3
IL_0028: ldloc.3
IL_0029: ldloc.2
IL_002a: call string [mscorlib]System.String::Concat(string,
string)
IL_002f: stloc.3
IL_0030: ldloc.3
IL_0031: call void [mscorlib]System.Console::WriteLine(string)
IL_0036: ret
} // end of method a::Main
string a=str1+str2+str3+str4只分配一次内存,而后者的代码需要分配三次内存。(来自CSDN上的讨论)
下面是对String的一些网上的分析:摘自.NET框架未公开的特性[String]
背景知识:
String 是.NET的一种基元类型。CLR和JIT为一些特殊的类作了特殊的处理和优化,String就是其中的一种。其他的包括:其他基元类型, StringBuilder,Array,Type,Enum,Delegate和一些Reflection类,比如MethodInfo。
在.NET 1.0中,所有分配到堆的对象都包含了两个东东:一个对象头(4字节)和一个指向方法表的指针(4字节)。对象头提供有5个位标志,其中一个位标志是为 GC保留的,它标识对象是否是“可到达对象“(“可到达对象“是垃圾回收算法里的一个名词,简单的说就是指正在被应用程序使用的对象)。剩余的27个位作 为一个索引,被称作syncindex,它指向一个表。这个索引有多种用途:首先,在使用"lock" 关键字时,它用于线程同步;另外,在调用Object.GetHashCode()方法时,它被用作缺省的哈希代码(继承类没有重写 Object.GetHashCode()时)。尽管这个索引不能为哈希代码提供最好的分布特性,但是,对于哈希代码的另外一个要求---具有相同值的对 象返回相同的哈希代码,它是可以满足的。在对象的整个生存周期,syncindex保持不变。
所有的对象都可以根据对象头计算得到它实际的内存占用,计算的公式如下(摘自Rotor包sscli20020326\sscli\clr\src\vm\object.h):
MT->GetBaseSize() + ((OBJECTTYPEREF->GetSizeField() * MT->GetComponentSize())
对于大多数对象,它们的大小是固定不变的。String类和Array类(包括Array的继承类)是仅有的两个可变长对象,也就是说它们创建后,对象的长度可以发生变化。
String有点类似OLE BSTRs---以长度数据开头,空字符结尾的Unicode字符数组。下面列出的是String在内部维护的三个字段:
[NonSerialized]private int m_arrayLength;
[NonSerialized]private int m_stringLength;
[NonSerialized]private char m_firstChar;
它们的具体含义如下表所示:
m_arrayLength
这是分配给字符串的实际长度(以字符为单位)。通常创建一个字符串时,m_arrayLength与字符串的逻辑长度(m_stringLength)是相同的。但是,如果使用StringBuilder返回一个字符串,实际长度可能比逻辑长度大。
m_stringLength
这是字符串的逻辑长度,你可以通过String.Length属性获得。出于优化性能的需要,m_stringLength的一些高位被用作标识 符,所以String的最大长度要比UInt32.Max小得多(32位操作系统)。这些标识符的其中一些用来指示String是否是简单字符(比如 plain ASCII),这样在排序和比较的时候,就不采用复杂的UNICODE算法。
m_firstChar
这是字符串的第一个字符。如果是空字符串的话,这是空字符。
String总是以一个空字符结尾,这一点加强了它与非托管代码和传统的Win32API的互操作性。
String总共占用的内存为:16字节+2字节*字符数+2字节(最后的空字符)。表1中已经讲述,如果使用StringBuilder来创建字符串,那么实际分配的内存将可能比String.Length大。
非常有效率的StringBuilder
与String密切关联的是StringBuilder。虽然StringBuilder被放在System.Text命名空间,但它不是一个平常的类。 运行时和JIT编译器对StringBuilder进行了特殊的处理,你想写一个跟它一样有效率的类是不容易的。
StringBuilder在内部维护一个字符串变量---m_StringValue,并且允许直接对它进行修改。默认情况下,m_StringValue的m_arrayLength字段为16。下面是StringBuilder维护的2个内部字段:
internal int m_MaxCapacity = 0;
internal String m_StringValue = null;
它们的具体含义如下所示:
m_MaxCapacity
StringBuilder的最大容量,它规定了最多可以放置到m_StringValue的字符个数,默认值为Int32.MaxValue。你可以自己规定一个小一点的容量,m_MaxCapacity一旦被指定就不能再更改。
m_StringValue
StringBuilder维护的一个字符串(Jeffrey Richter在《Applied Microsoft .NET Framework Programming》中讲述StringBuilder维护的是一个字符数组,译者认为作为字符数组比较容易理解,但从Rotor包的源代码看,实际 维护的应该是一个字符串
使用StringBuilder.ToString()返回一个字符串时,实际的字符串被返回(也就是说该方法返回的是StringBuilder内部维 护的字符串字段 (m_StringValue)的引用,而不是创建新字符串)。如果StringBuilder的容量(ArrayLength)超过实际字符数的两倍以 上,StringBuilder.ToString()将返回一个字符串的简洁版本。在调用了StringBuilder. ToString()方法之后,再次修改StringBuilder将会产生一个复制动作,它将创建一个新的字符串;这时被修改的是新的字符串,如此,原 来已经返回的字符串才不会发生改变。
除了字符串使用的内存外,StringBuilder额外开销了16个字节,但是,同样的StringBuilder对象可以被使用多次来生成多个字符串,这样StringBuilder仅仅带来一次额外开销。
我们可以看到,使用StringBuilder是非常有效率的。