深入理解 StringBuilder

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);  // 2147483647
MaxCapacity是只读属性,其取值范围为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);  //  32

Append等扩充字符串操作时,如果结果字符串的长度大于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; 
    } 
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值