幾乎所有的資料都告訴我們,在頻繁進行字符串拼接操作的時候推薦使用StringBuilder,因為它提供更優秀的性能,不辭勞苦的我們也寫示例驗證過,確實如傳說的那樣!但為什么StringBuilder 在操作字符串會有優異的表示呢?它真是像很多資料中所說“每次新追加進來字符串和舊有字符串總長度超設定容量時,會新鍵一個數組存放串字符,並且丟棄原有舊數組”嗎?這一節我們來看個究竟。
在前面的章節中我們已經知道,字符串是由字符組成,由於字符串具有不可變性,所以每一次對字符串的變動都會重新分配內存、創建一個字符串對象、丟棄舊對象,在重新分配內存過程可能會導致垃圾回收,這一系列的操作,會大大損傷性能。為了解決這個問題,FCL提供了一個System.Text.StringBuilder類來構造管理字符串。就像它的名字一樣,它是一個構造器,提供了對字符串的追加、移除和替換等功能,向StringBuilder對象追加字符串時,實際上在內部是轉為追加字符。System.Text.StringBuilder類有幾個重要的字段、屬性和方法,我們一個一個來看。
(1) internal char[] m_ChunkChars
保存StringBuilder所管理着的字符串中的字符。系統默認初始化它的長度為16,當新追加進來的字符串長度與舊有字符串長度之和大於該字符數組容量時,新創建字符數組容量會增加到“2的(n+1)次冪”(假如當前字符數組容量為2的n次冪)。
(2) internal int m_ChunkLength;
字符數組m_ChunkChars內的實際字符個數,系統默認m_ChunkChars的容量是16。
(3) internal int m_ChunkOffset;
字符定位的偏移量
(4) internal StringBuilder m_ChunkPrevious;
內部的一個StringBuilder對象,追加的字符串長度和舊字符串長度之合大於字符數組m_ChunkChars的最大容量時,會根據當前的(this)StringBuilder創建一個新的StringBuilder對象,將m_ChunkPrevious指向新創建的StringBuilder對象。這個是關鍵。
(5) public int Length;
當前StringBuilder對象實際管理的字符串長度。Length = m_ChunkLength + m_ChunkOffset
(6) public int Capacity;
設置或獲取字符數組m_ChunkChars的最大容量。
創建一個新的StringBuilder對象后,字符數組m_ChunkChars最大容量被初始化為16,向StringBuilder對象追加字符串,如果追加前后的字符串總長度小於等於16,則將新追加的字符串的字符復制到m_ChunkChars數組;如果追加前后的字符串總長度大於16,則先用新字符將當前m_ChunkChars填滿,再以當前對象(this)為基礎構造一個StringBuilder對象,並且將m_ChunkPrevious指向這個新創建的StringBuilder對象,然后將Capacity設置為2的(n+1)次冪32,重新初始化字符數組m_ChunkChars且容量為2的(n+1)次冪32(注意:這個不一定),然后將剛才剩余的字符復制到最新的字符數組m_ChunkChars。每一次追加字符串都會執行上面類似的步驟。下面我們來看一下這個過程,為了方便演示,我們在創建一個StringBuilder對象后,先設置容量為2。如下代碼:
StringBuilder strBuilder = new StringBuilder(); strBuilder.Capacity = 2;
看一下初始化后的結果:
可以看到最大容量Capacity為2,由於未向其追加字符串,所以字符數組m_ChunkChars的元素為空,m_ChunkPrevious是null。
A)接着我們向其追加一個字符串”a”:
可以看到,新添加的字符a被放到了字符數組的0號位置,字符數組內元素個數為1。
B)接着追加一個字符b:
此時是將新字符b放到了字符數組1號位置,很顯然字符數組的有效長度m_ChunkLength增加1后值為2,此時的m_chunkPrevious依然保持着null。
C)接着我們再添加一個字符c:
可以看到,strBuilder的字符容量Capacity已經變成2的(1+1)次冪4。因為原先長度為2的數組m_ChunkChars已經無法裝載長度為3的字符串,所以要創新創建一個數組來擴容,但是這里使用舊有容量(值為2)創建的數組已經中以容納新加進來的字符串 c ,所以m_ChunkChars數組依然被初始化為容量為2的數組。由於strBuilder內已經有3個字符,Length=m_ChunkLength+m_ChunkOffset,所以Length為3,最新的字符c已經放到了新數組m_ChunkChars的0號位。最主要的是字段m_ChunkPrevious已經不空null了,它已經指向截止到B)步驟的strBuilder對象,這個指向可以通過StringBuilder內部代碼看的出來:
private void ExpandByABlock(int minBlockCharCount) { if ((minBlockCharCount + this.Length) > this.m_MaxCapacity) { throw new ArgumentOutOfRangeException("requiredLength", Environment.GetResourceString("ArgumentOutOfRange_SmallCapacity")); } 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; if ((this.m_ChunkOffset + num) < num) { this.m_ChunkChars = null; throw new OutOfMemoryException(); } this.m_ChunkChars = new char[num]; }
事實上初始化m_ChunkPrevious在前,創建新的字符數組m_ChunkChars在后,最后才是復制字符到數組m_ChunkChars中。
D)接着我們連續添加兩個字符d和e:
在上一步驟C)的時候容量Capacity是4,字符數組還有一個空位置,所以當我們添加字符d時還可以用該數組,並不需要遷移對象和重建數組。但是在添加字符e的時候,由於總字符個數為5(abcde)已經超出了Capacity的4,所以此時會執行類似C)的步驟,最關鍵的兩行代碼:
this.m_ChunkPrevious = new StringBuilder(this); this.m_ChunkChars = new char[num];
需要說明,為了節省內存,StringBuilder內部並不一定是每次擴容m_ChunkChars真的按照2的(n+1)次冪進行計算,它是根據舊有字符串和新追加字符串的總長度和上一次容量的差來進行擴容:
int num = Math.Max(minBlockCharCount, Math.Min(this.Length, 0x1f40)); this.m_ChunkChars = new char[num];
歸根結底,StringBuilder是在內部以字符數組m_ChunkChars為基礎維護一個鏈表m_ChunkPreviou。如圖:
(1) Append方法及重載
public unsafe StringBuilder Append(string value)
向StringBuilder追加新元素,由於在內部使用了指針,所以這里用了unsafe。它有18個重載,無論哪個重載方法,最終都是將新值轉為字符進行添加。類似的還有AppendFormat系列方法。
(2) Insert方法及重載
public unsafe StringBuilder Insert(int index, string value)
向指定位置插入字符串。
(3) Replace方法及重載
public StringBuilder Replace(string oldValue, string newValue);
使用新字符串替換與oldValue匹配的字符串,它有3個重載。
(4) Remove方法
public StringBuilder Remove(int startIndex, int length);
從指定索引位移除指定數量的字符,它沒有重載。方法Insert、Replace和Remove都是對內部字符數組m_ChunkChar和鏈表中m_ChunkPrevious內的字符數組m_ChunkChar操作,StringBuilder內部實現有點“繞”,感興趣的可以自行去研究研究。
(5) ToString方法
public override string ToString();
StringBuilder重寫了基類的ToString()方法用來獲取StringBuilder對象的字符串表示,它是將鏈表m_ChunkPrevious中的字符數組m_ChunkChars及當前StringBuilder對象的字符數組m_ChunkChar中的字符轉成String對象返回,這一步是創建一個新的String對象,所以對這個String對象(ToString()的結果)的操作不會影響到StringBuilder對象內部的字符。
還有一個方法與ToString()方法類似:
public string ToString(int startIndex, int length);
將指定位置及指定長度的字符轉為字符串。