首页 > 学院 > 开发设计 > 正文

深入理解 StringBuilder

2019-11-08 18:36:32
字体:
来源:转载
供稿:网友
public sealed class StringBuilder : ISerializable位于:System.Text命名空间中。StringBuilder仅实现ISerializable接口,直接派生自Object,相对于String类型其功能不太完善,如ToUpper、SubString、foreach遍历每个字符等等,后面介绍如何扩展其功能。它是密封类型,不能通过派生它的子类来改变其行为。StringBuilder能够动态高效的构建字符串、修改字符串,不能保证所有实例成员都是线程安全的,尽管在类型定义中加入了很多线程安全的控制,如果要确保其线程安全,须手工实现线程同步机制。StringBuilder内部维护的是一个String对象,在String类型所在的程序外部,String表现出不变性,但在String类型的内部,定义了一些改变String对象的方法,声明为internal,这些方法供StringBuilder等调用。最大容量(MaxCapacity):默认的最大容量等于int.MaxValue。StringBuilder sb = new StringBuilder(); Console.WriteLine(sb.MaxCapacity); // 2147483647MaxCapacity是只读属性,其取值范围为1~int.MaxValue,如果想设定自己的最大容量,StringBuild提供了一个构造方法:public StringBuilder(int capacity, int maxCapacity)在构造StringBuilder对象时设定最大容量值,最大值一旦设定,将不可改变,如果Append或其它操作使Length大于MaxCapacity将抛出异常。容量(Capacity):返回StringBuilder的当前容量,其取值范围为:0~MaxCapacity,这个属性是可读写的,若设置的值小于Length将抛出异常。长度(Length):返回StringBuilder内部维护的当前String对象的长度,取值范围:0~MaxCapacity,可变属性。int.MaxValue >= MaxCapacity >= Capacity >= Length >=0 (MaxCapacity >= 1)MaxCapacity只是一个Capacity和Length的范围约束,Capacity是StringBuilder内部字符串实际分配内存的大小,Length是StringBuilder内部字符串的有效字符的数量。Capacity的变化规律StringBuilder的Capacity属性的取值范围是:0~MaxCapacity;默认大小为:16。StringBuilder sb = new StringBuilder(); Console.WriteLine(sb.Capacity); // 16当以构造方法StringBuilder(String str) 创建StringBuilder对象时,Capacity的值可用下面的伪代码表示:IF str.Length < 16 THEN sb.Capacity = 16 ELSE     BEGIN     sb.Capacity = 16     WHILE str.Length < sb.Capacity         sb.Capacity *= 2    END示例:StringBuilder sb = new StringBuilder(new string('a', 16)); Console.WriteLine(sb.Capacity); // 16 StringBuilder sb1 = new StringBuilder(new string('a', 18)); Console.WriteLine(sb1.Capacity); // 32Append等扩充字符串操作时,如果结果字符串的长度大于Capacity,则Capacity加倍;如果加倍后的Capacity还不足以容纳结果字符串,则Capacity的值等于结果字符串的长度。StringBuilder sb1 = new StringBuilder(new string('a', 18)); Console.WriteLine(sb1.Capacity);                              // 32 Console.WriteLine(sb1.Append(new string('b', 18)).Capacity);  // 64  (32 * 2) Console.WriteLine(sb1.Append(new string('c', 100)).Capacity); // 136 (result.Length) Console.WriteLine(sb1.Append(new string('d', 100)).Capacity); // 272 (136 * 2)实现原理:StringBuilder维护一个长度等于Capacity的字符串(可以看作字符数组),当Capacity长度的字符串不足以容纳结果字符串时,StringBuilder开辟新的长度为经过上面的规则计算好的Capacity的内存区域,将原字符串复制到新的内存区域再进行操作,原字符串区域交给GC回收。因此这里也涉及到内存的分配与回收,使用StringBuilder时最好估算一下所需容量,用这个容量初始化Capacity,提高性能。StringBuilder内字符串的垃圾数据字符串在内存中是顺序存储的。StringBuilder内部字符串的可用长度是Capacity,有效字符数是Length。刚刚构造StringBuilder后Length到Capacity的范围,都保留内存中的原垃圾数据。初始化后显式增大Capacity大小后,增大的部分内存保留原垃圾数据。系统自动扩大容量,Length到Capacity的部分将清空内存中原垃圾数据为'/0'。设置StringBuilder长度时,若新设置的Lenght小于原Length,字符串将被截断,新Length到原Length的部分填充空字符'/0';若新设置的Lenght大于原Length,原Lenght到新Length的部分填充空字符'/0'。由于字符串在内存中是顺序存储的,可用下面的方法查看StringBuilder内部字符串内存中的数据:unsafe static void ShowContent(StringBuilder sb) {     fixed(char* ch = sb.ToString())     {         for(int i = 0; i < sb.Capacity; i++)         {             Console.Write((int)ch[i] + " ");         }     } }ToString方法由下面的示例代码可以看出,每调用一次ToString(),获得的String对象引用都会变化。static void Main(string[] args) {     StringBuilder sb = new StringBuilder("Hello StringBuilder!");     ShowAddress(sb.ToString()); // 20656316     ShowAddress(sb.ToString()); // 20666276     ShowAddress(sb.ToString()); // 20666372     ShowAddress(sb.ToString()); // 20666468     ShowAddress(sb.ToString()); // 20666564     ShowAddress(sb.ToString()); // 20666660     ShowAddress(sb.ToString()); // 20666756     ShowAddress(sb.ToString()); // 20666852     ShowAddress(sb.ToString()); // 20666948     ShowAddress(sb.ToString()); // 20667044 } public unsafe static void ShowAddress(string s) {     fixed (char* p = s)     {         Console.WriteLine((int)p);     } }为得出原因,Reflector查看,是如下代码:public override string ToString() {     string stringValue = this.m_StringValue;     if (this.m_currentThread != Thread.InternalGetCurrentThread())     {         return string.InternalCopy(stringValue);     }     if ((2 * stringValue.Length) < stringValue.ArrayLength)     {         return string.InternalCopy(stringValue);     }     stringValue.ClearPostNullChar();     this.m_currentThread = IntPtr.Zero;     return stringValue; }1.看来ToString()方法对线程安全进行控制了,如果不是当前线程访问,返回字符串的拷贝。2.ArrayLength应该就是Capacity了,如果长度小于Capacity的1/2,为优化性能,返回字符串的新的拷贝。3.以上条件不满足,返回StringBuilder内部字符串。但调用一次ToString()后,执行了this.m_currentThread = IntPtr.Zero; ,如果再紧接着执行ToString()会返回新的字符串。要返回StringBuilder内部字符串的真实地址,可用反射或序列化取得StringBuilder内的字符串的引用,再取字符串的地址,StringBuilder内字符串声明为:internal volatile string m_StringValue;StringBuilder sb = new StringBuilder("Hello StringBuilder!"); ShowAddress(sb.ToString()); // 21075152 ShowAddress(sb.ToString()); // 21117944 ShowAddress(sb.ToString()); // 21118040 ShowAddress(sb.ToString()); // 21075152 SerializationInfo info = new SerializationInfo(     typeof(StringBuilder), new FormatterConverter()); ((ISerializable)sb).GetObjectData(     info, new StreamingContext()); String s = info.GetString("m_StringValue"); ShowAddress(s); // 21075152可见,第一次调用ToString()方法非常高效,直接返回StringBuilder内字符串的引用。如果没有重新取得m_currentThread,接下来的调用会拷贝构造新的字符串。CLR会记录该StringBuilder维护的String已被引用,如果试图对其修改,StringBuilder会重新分配内存区域,将原字符串拷贝到新的内存区域然后进行修改。ToString重载的另一个版本原型为:public string ToString(int startIndex, int length)会构造新的字符串,新字符串值为:起始索引为startIndex,长度为length的子字符串,这个重载能实现String中的SubString的功能。EnsureCapacity确保最小的容量不小于给定的数值。如果给定的数值值小于目前的Capacity,则忽略给定的数值;如果给定的数值大于Capacity,则设置Capacity为给定的数值。 AppendFormat有时感觉StringBuilder连接字符串不如String连接方便,如果用AppendFormat会方便很多,使用方法跟String.Format相似,这个方法保证了字符串连接的优雅与高效。不知道大家有没有注意到,Append、AppendFormat、AppendLine、Insert、Remove、Replace等方法对StringBuild对象操作完成后都返回StringBuilder自身,这样的设计便于进行一连串的操作。如:StringBuilder sb0 = new StringBuilder("abc",10); string s = sb0.Append("abc").     Replace("ca", "--").     Insert(0, "String:").     ToString();扩展StringBuilder的操作StringBuilder与String相比,非常多的操作没有实现,可以调用ToString()后再进行操作,但这样会影响效率;也可以用扩展方法来扩展其操作;既然上面能取得StringBuilder成员m_StringValue的值,可以直接用所有String的方法来处理判断,但这样会造成一些问题,不推荐这样做。扩展StringBuilder,如foreach遍历所有字符、每个字符字符转换为其对应的大写字符示例:static class PRogram {     static void Main(string[] args)     {         StringBuilder sb = new StringBuilder("Hello StringBuilder!");         foreach (var c in sb.GetEnumerator())         {             Console.WriteLine(c);         }         Console.WriteLine(sb.ToUpper());         Console.ReadKey();     }     static IEnumerable<Char> GetEnumerator(this StringBuilder sb)     {         for (var i = 0; i < sb.Length; i++)         {             yield return sb[i];         }     }     static StringBuilder ToUpper(this StringBuilder sb)     {         for (var i = 0; i < sb.Length; i++)         {             sb[i] = Char.ToUpper(sb[i]);         }         return sb;     } }
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表