簡(jiǎn)單聊聊C#字符串構(gòu)建利器StringBuilder
前言
在日常的開(kāi)發(fā)中StringBuilder大家肯定都有用過(guò),甚至用的很多。畢竟大家都知道一個(gè)不成文的規(guī)范,當(dāng)需要高頻的大量的構(gòu)建字符串的時(shí)候StringBuilder的性能是要高于直接對(duì)字符串進(jìn)行拼接的,因?yàn)橹苯邮褂?或+=都會(huì)產(chǎn)生一個(gè)新的String實(shí)例,因?yàn)镾tring對(duì)象是不可變的對(duì)象,這也就意味著每次對(duì)字符串內(nèi)容進(jìn)行操作的時(shí)候都會(huì)產(chǎn)生一個(gè)新的字符串實(shí)例,這對(duì)大量的進(jìn)行字符串拼接的場(chǎng)景是非常不友好的。因此StringBuilder孕育而出。這里需要注意的是,這并不意味著可以用StringBuilder來(lái)代替所有字符串拼接的的場(chǎng)景,這里我們強(qiáng)調(diào)一下是頻繁的對(duì)同一個(gè)字符串對(duì)象進(jìn)行拼接的操作。今天我們就來(lái)看一下c#中StringBuilder的巧妙實(shí)現(xiàn)方式,體會(huì)一下底層類(lèi)庫(kù)解決問(wèn)題的方式。
需要注意的是,這里的不可變指的是字符串對(duì)象本身的內(nèi)容是不可改變的,但是字符串變量的引用是可以改變的。
簡(jiǎn)單示例
接下來(lái)咱們就來(lái)簡(jiǎn)單的示例一下操作,其實(shí)核心操作主要是Append方法和ToString方法,源碼的的角度上來(lái)說(shuō)還有StringBuilder的構(gòu)造函數(shù)。首先是大家最常用的方式,直接各種Append然后最后得到結(jié)果。
StringBuilder builder = new StringBuilder();
builder.Append("我和我的祖國(guó)");
builder.Append(',');
builder.Append("一刻也不能分割");
builder.Append('。');
builder.Append("無(wú)論我走到哪里,都留下一首贊歌。");
builder.Append("我歌唱每一座高山,我歌唱每一條河。");
builder.Append("裊裊炊煙,小小村落,路上一道轍。");
builder.Append("我永遠(yuǎn)緊依著你的心窩,你用你那母親的脈搏,和我訴說(shuō)。");
string result = builder.ToString();
Console.WriteLine(result);
StringBuilder也是支持通過(guò)構(gòu)造函數(shù)初始化一些數(shù)據(jù)的,有沒(méi)有在構(gòu)造函數(shù)傳遞初始化數(shù)據(jù),也就意味著不同的初始化邏輯。比如以下操作
StringBuilder builder = new StringBuilder("我和我的祖國(guó)");
//或者是指定StringBuilder的容量,這樣的話(huà)StringBuilder初始可承載字符串的長(zhǎng)度是16
builder = new StringBuilder(16);
因?yàn)镾tringBuilder是基礎(chǔ)類(lèi)庫(kù),因此看著很簡(jiǎn)單,用起來(lái)也很簡(jiǎn)單,而且大家也都經(jīng)常使用這些操作。
源碼探究
上面咱們簡(jiǎn)單的演示了StringBuilder的使用方式,一般的類(lèi)似的StringBuilder或者是List這種雖然我沒(méi)使用的過(guò)程中可以不關(guān)注容器本身的長(zhǎng)度一直去添加元素,實(shí)際上這些容器的本身內(nèi)部實(shí)現(xiàn)邏輯都包含了一些擴(kuò)容相關(guān)的邏輯。上面咱們提到了一下StringBuilder的核心主要是三個(gè)操作,也就是通過(guò)這三個(gè)功能可以呈現(xiàn)出StringBuilder的工作方式和原理。
- 一個(gè)是構(gòu)造函數(shù),因?yàn)闃?gòu)造函數(shù)包含了初始化的一些邏輯。
- 其次是Append方法,這是StringBuilder進(jìn)行字符串拼接的核心操作。
- 最后是將StringBuilder轉(zhuǎn)換成字符串的操作ToString方法,這是我們得到拼接字符串的操作。
接下來(lái)咱們就從這三個(gè)相關(guān)的方法入手來(lái)看一下StringBuilder的核心實(shí)現(xiàn),這里我參考的.net版本為v6.0.2。
構(gòu)造入手
我們上面提到了StringBuilder的構(gòu)造函數(shù)代表了初始化邏輯,大概來(lái)看就是默認(rèn)的構(gòu)造函數(shù),即默認(rèn)初始化邏輯和自定義一部分構(gòu)造函數(shù)的邏輯,主要是的邏輯是決定了StringBuilder容器可容納字符串的長(zhǎng)度。
無(wú)參構(gòu)造
首先來(lái)看一下默認(rèn)的無(wú)參構(gòu)造函數(shù)的實(shí)現(xiàn)[點(diǎn)擊查看源碼??]
//可承載字符的最大容量,即可以拼接的字符串的長(zhǎng)度
internal int m_MaxCapacity;
//承載【拼接字符串的char數(shù)組
internal char[] m_ChunkChars;
//默認(rèn)的容量,即默認(rèn)初始化m_ChunkChars的長(zhǎng)度,也就是首次擴(kuò)容觸發(fā)的長(zhǎng)度
internal const int DefaultCapacity = 16;
public StringBuilder()
{
m_MaxCapacity = int.MaxValue;
m_ChunkChars = new char[DefaultCapacity];
}
通過(guò)默認(rèn)的無(wú)參構(gòu)造函數(shù),我們可以了解到兩點(diǎn)信息
- 首先是StringBuilder核心存儲(chǔ)字符串的容器是char[]字符數(shù)組。
- 默認(rèn)容器的char[]字符數(shù)組聲明的長(zhǎng)度是16,即如果首次StringBuilder容納的字符個(gè)數(shù)超過(guò)16則觸發(fā)擴(kuò)容機(jī)制。
帶參數(shù)的構(gòu)造
StringBuilder的有參數(shù)的構(gòu)造函數(shù)有好幾個(gè),如下所示
//聲明初始化容量,即首次擴(kuò)容觸發(fā)的長(zhǎng)度條件 public StringBuilder(int capacity) //聲明初始化容量,和最大容量即可以動(dòng)態(tài)構(gòu)建字符串的總長(zhǎng)度 public StringBuilder(int capacity, int maxCapacity) //用給定字符串初始化 public StringBuilder(string? value) //用給定字符串初始化,并聲明容量 public StringBuilder(string? value, int capacity) //用一個(gè)字符串截取指定長(zhǎng)度初始化,并聲明最大容量 public StringBuilder(string? value, int startIndex, int length, int capacity)
雖然構(gòu)造函數(shù)有很多,但是大部分都是在調(diào)用調(diào)用自己的重載方法,核心的有參數(shù)的構(gòu)造函數(shù)其實(shí)就兩個(gè),咱們分別來(lái)看一下,首先是指定容量的初始化構(gòu)造函數(shù)[點(diǎn)擊查看源碼??]
//可承載字符的最大容量,即可以拼接的字符串的長(zhǎng)度
internal int m_MaxCapacity;
//承載【拼接字符串的char數(shù)組
internal char[] m_ChunkChars;
//默認(rèn)的容量,即默認(rèn)初始化m_ChunkChars的長(zhǎng)度,也就是首次擴(kuò)容觸發(fā)的長(zhǎng)度
internal const int DefaultCapacity = 16;
public StringBuilder(int capacity, int maxCapacity)
{
//指定容量不能大于最大容量
if (capacity > maxCapacity)
{
throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_Capacity);
}
//最大容量不能小于1
if (maxCapacity < 1)
{
throw new ArgumentOutOfRangeException(nameof(maxCapacity), SR.ArgumentOutOfRange_SmallMaxCapacity);
}
//初始化容量不能小于0
if (capacity < 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity), SR.Format(SR.ArgumentOutOfRange_MustBePositive, nameof(capacity)));
}
//如果指定容量等于0,則使用默認(rèn)的容量
if (capacity == 0)
{
capacity = Math.Min(DefaultCapacity, maxCapacity);
}
//最大容量賦值
m_MaxCapacity = maxCapacity;
//分配指定容量的數(shù)組
m_ChunkChars = GC.AllocateUninitializedArray<char>(capacity);
}
主要就是對(duì)最大容量和初始化容量進(jìn)行判斷和賦值,如果制定了初始容量和最大容量則以傳遞進(jìn)來(lái)的為主。接下來(lái)再看一下根據(jù)指定字符串來(lái)初始化StringBuilder的主要操作[點(diǎn)擊查看源碼??]
//可承載字符的最大容量,即可以拼接的字符串的長(zhǎng)度
internal int m_MaxCapacity;
//承載【拼接字符串的char數(shù)組
internal char[] m_ChunkChars;
//默認(rèn)的容量,即默認(rèn)初始化m_ChunkChars的長(zhǎng)度,也就是首次擴(kuò)容觸發(fā)的長(zhǎng)度
internal const int DefaultCapacity = 16;
//當(dāng)前m_ChunkChars字符數(shù)組中已經(jīng)使用的長(zhǎng)度
internal int m_ChunkLength;
public StringBuilder(string? value, int startIndex, int length, int capacity)
{
if (capacity < 0)
{
throw new ArgumentOutOfRangeException();
}
if (length < 0)
{
throw new ArgumentOutOfRangeException();
}
if (startIndex < 0)
{
throw new ArgumentOutOfRangeException();
}
//初始化的字符串可以為null,如果為null則只用空字符串即""
if (value == null)
{
value = string.Empty;
}
//基礎(chǔ)長(zhǎng)度判斷,這個(gè)邏輯其實(shí)已經(jīng)包含了針對(duì)字符串截取的起始位置和接要截取的長(zhǎng)度進(jìn)行判斷了
if (startIndex > value.Length - length)
{
throw new ArgumentOutOfRangeException();
}
//最大容量是int的最大值,即2^31-1
m_MaxCapacity = int.MaxValue;
if (capacity == 0)
{
capacity = DefaultCapacity;
}
//雖然傳遞了默認(rèn)容量,但是這里依然做了判斷,在傳遞的默認(rèn)容量和需要存儲(chǔ)的字符串容量總?cè)∽畲笾?
capacity = Math.Max(capacity, length);
//分配指定容量的數(shù)組
m_ChunkChars = GC.AllocateUninitializedArray<char>(capacity);
//這里記錄了m_ChunkChars固定長(zhǎng)度的快中已經(jīng)被使用的長(zhǎng)度
m_ChunkLength = length;
//把傳遞的字符串指定位置指定長(zhǎng)度(即截取操作)copy到m_ChunkChars中
value.AsSpan(startIndex, length).CopyTo(m_ChunkChars);
}
這個(gè)初始化操作主要是截取給定字符串的指定長(zhǎng)度,存放到ChunkChars用于初始化StringBuilder,其中初始化的容量取決于可以截取的長(zhǎng)度是否大于指定容量,實(shí)質(zhì)是以能夠存放截取長(zhǎng)度的字符串為主。
構(gòu)造小結(jié)
通過(guò)StringBuilder的構(gòu)造函數(shù)中的邏輯我們可以看到StringBuilder本質(zhì)存儲(chǔ)是在char[],這個(gè)字符數(shù)組的初始化長(zhǎng)度是16,這個(gè)長(zhǎng)度主要的作用是擴(kuò)容機(jī)制,即首次需要進(jìn)行擴(kuò)容的時(shí)機(jī)是當(dāng)m_ChunkChars長(zhǎng)度超過(guò)16的時(shí)候,這個(gè)時(shí)候原有的m_ChunkChars已經(jīng)不能承載需要構(gòu)建的字符串的時(shí)候觸發(fā)擴(kuò)容。
核心方法
我們上面看到了StringBuilder相關(guān)的初始化代碼,通過(guò)初始化操作,我們可以了解到StringBuilder本身的數(shù)據(jù)結(jié)構(gòu),但是想了解StringBuilder的擴(kuò)容機(jī)制,還需要從它的Append方法入手,因?yàn)橹挥蠥ppend的時(shí)候才有機(jī)會(huì)去判斷原有的m_ChunkChars數(shù)組長(zhǎng)度是否滿(mǎn)足存儲(chǔ)Append進(jìn)來(lái)的字符串。關(guān)于StringBuilder的Append方法有許多重載,這里咱們就不逐個(gè)列舉了,但是本質(zhì)都是一樣的。因此咱們就選取咱們最熟悉的和最常用的Append(string? value)方法進(jìn)行講解,直接找到源碼位置[點(diǎn)擊查看源碼??]
//承載【拼接字符串的char數(shù)組
internal char[] m_ChunkChars;
//當(dāng)前m_ChunkChars字符數(shù)組中已經(jīng)使用的長(zhǎng)度
internal int m_ChunkLength;
public StringBuilder Append(string? value)
{
if (value != null)
{
// 獲取當(dāng)前存儲(chǔ)塊
char[] chunkChars = m_ChunkChars;
// 獲取當(dāng)前塊已使用的長(zhǎng)度
int chunkLength = m_ChunkLength;
// 獲取傳進(jìn)來(lái)的字符的長(zhǎng)度
int valueLen = value.Length;
//當(dāng)前使用的長(zhǎng)度 + 需要Append的長(zhǎng)度 < 當(dāng)前塊的長(zhǎng)度 則不需要擴(kuò)容
if (((uint)chunkLength + (uint)valueLen) < (uint)chunkChars.Length)
{
//判斷傳進(jìn)來(lái)的字符串長(zhǎng)度是否<=2
//如果小于2則只用直接訪(fǎng)問(wèn)位置的方式操作
if (valueLen <= 2)
{
//判斷字符串長(zhǎng)度>0的場(chǎng)景
if (valueLen > 0)
{
//m_ChunkChars的已使用長(zhǎng)度其實(shí)就是可以Append新元素的起始位置
//直接取value得第0個(gè)元素放入m_ChunkChars[可存儲(chǔ)的起始位置]
chunkChars[chunkLength] = value[0];
}
//其實(shí)是判斷字符串長(zhǎng)度==2的場(chǎng)景
if (valueLen > 1)
{
//因?yàn)樯厦嬉呀?jīng)取了value第0個(gè)元素放入了m_ChunkChars中
//現(xiàn)在則取value得第1個(gè)元素繼續(xù)放入chunkLength的下一位置
chunkChars[chunkLength + 1] = value[1];
}
}
else
{
//如果value的長(zhǎng)度大于2則通過(guò)操作內(nèi)存去追加value
//獲取m_ChunkChars的引用位置,偏移到m_ChunkLength的位置追加value
Buffer.Memmove(
ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(chunkChars), chunkLength),
ref value.GetRawStringData(),
(nuint)valueLen);
}
//更新以使用長(zhǎng)度的值,新的使用長(zhǎng)度是當(dāng)前已使用長(zhǎng)度+追加進(jìn)來(lái)的字符串長(zhǎng)度
m_ChunkLength = chunkLength + valueLen;
}
else
{
//走到這里說(shuō)明進(jìn)入了擴(kuò)容邏輯
AppendHelper(value);
}
}
return this;
}
這一部分邏輯主要展示了未達(dá)到擴(kuò)容條件時(shí)候的邏輯,其本質(zhì)就是將Append進(jìn)來(lái)的字符串追加到m_ChunkChars數(shù)組里去,其中m_ChunkLength代表了當(dāng)前m_ChunkChars已經(jīng)使用的長(zhǎng)度,另一個(gè)含義也是代表了下一次Append進(jìn)來(lái)元素存儲(chǔ)到m_ChunkLength的起始位置。而擴(kuò)容的需要的邏輯則進(jìn)入到了AppendHelper方法中,咱們看一下AppendHelper方法的實(shí)現(xiàn)[點(diǎn)擊查看源碼??]
private void AppendHelper(string value)
{
unsafe
{
//防止垃圾收集器重新定位value變量。
//指針操作,string本身是不可變的char數(shù)組,所以它的指針是char*
fixed (char* valueChars = value)
{
//調(diào)用了另一個(gè)append
Append(valueChars, value.Length);
}
}
}
這里是獲取了傳遞進(jìn)來(lái)的value指針然后調(diào)用了另一個(gè)重載的Append方法,不過(guò)從這段代碼中可以得到一個(gè)信息這個(gè)操作是非線(xiàn)程安全的。我們繼續(xù)找到另一個(gè)Append方法[點(diǎn)擊查看源碼??]
public unsafe StringBuilder Append(char* value, int valueCount)
{
// value必須有值
if (valueCount < 0)
{
throw new ArgumentOutOfRangeException();
}
//新的長(zhǎng)度=StringBuilder的長(zhǎng)度+需要追加的字符串長(zhǎng)度
int newLength = Length + valueCount;
//新的長(zhǎng)度不能大于最大容量
if (newLength > m_MaxCapacity || newLength < valueCount)
{
throw new ArgumentOutOfRangeException();
}
// 新的起始位置=需要追加的長(zhǎng)度+當(dāng)前使用的長(zhǎng)度
int newIndex = valueCount + m_ChunkLength;
// 判斷當(dāng)前m_ChunkChars的容量是否夠用
if (newIndex <= m_ChunkChars.Length)
{
//夠用的話(huà)則直接將追加的元素添加到m_ChunkChars中去
new ReadOnlySpan<char>(value, valueCount).CopyTo(m_ChunkChars.AsSpan(m_ChunkLength));
//更新已使用的長(zhǎng)度為新的長(zhǎng)度
m_ChunkLength = newIndex;
}
//當(dāng)前m_ChunkChars不滿(mǎn)足存儲(chǔ)則需要擴(kuò)容
else
{
// 判斷當(dāng)前存儲(chǔ)塊m_ChunkChars還有多少未存儲(chǔ)的位置
int firstLength = m_ChunkChars.Length - m_ChunkLength;
if (firstLength > 0)
{
//把需要追加的value中的前firstLength位字符copy到m_ChunkChars中剩余的位置
//合理的利用存儲(chǔ)空間,截取需要追加的value到m_ChunkChars剩余的位置
new ReadOnlySpan<char>(value, firstLength).CopyTo(m_ChunkChars.AsSpan(m_ChunkLength));
//更新已使用的位置,這個(gè)時(shí)候當(dāng)前存塊m_ChunkChars已經(jīng)存儲(chǔ)滿(mǎn)了
m_ChunkLength = m_ChunkChars.Length;
}
// 獲取value中未放入到m_ChunkChars(因?yàn)楫?dāng)前塊已經(jīng)放滿(mǎn))剩余部分起始位置
int restLength = valueCount - firstLength;
//擴(kuò)展當(dāng)前存儲(chǔ)塊即擴(kuò)容操作
ExpandByABlock(restLength);
//判斷新的存儲(chǔ)塊是否創(chuàng)建成功
Debug.Assert(m_ChunkLength == 0, "A new block was not created.");
// 將value中未放入到m_ChunkChars的剩余部放入擴(kuò)容后的m_ChunkChars中去
new ReadOnlySpan<char>(value + firstLength, restLength).CopyTo(m_ChunkChars);
// 更新當(dāng)前已使用長(zhǎng)度
m_ChunkLength = restLength;
}
//一些針對(duì)當(dāng)前StringBuilder的校驗(yàn)操作,和相關(guān)邏輯無(wú)關(guān)不做詳細(xì)介紹
//類(lèi)似的Debug.Assert(m_ChunkOffset + m_ChunkChars.Length >= m_ChunkOffset, "The length of the string is greater than int.MaxValue.");
AssertInvariants();
return this;
}
這里的源代碼涉及到了一個(gè)StringBuilder的長(zhǎng)度問(wèn)題,Length代表著當(dāng)前StringBuilder對(duì)象實(shí)際存放的字符長(zhǎng)度,它的定義如下所示
public int Length
{
//StringBuilder已存儲(chǔ)的長(zhǎng)度=塊的偏移量+當(dāng)前塊使用的長(zhǎng)度
get => m_ChunkOffset + m_ChunkLength;
set
{
//注意這里是有代碼的只是我們暫時(shí)省略set邏輯
}
}
上面源碼的這個(gè)Append方法其實(shí)是另一個(gè)重載方法,只是Append(string? value)調(diào)用了這個(gè)邏輯,這里可以清晰的看到,如果當(dāng)前存儲(chǔ)塊滿(mǎn)足存儲(chǔ),則直接使用。如果當(dāng)前存儲(chǔ)位置不滿(mǎn)足存儲(chǔ),那么存儲(chǔ)空間也不會(huì)浪費(fèi),按照當(dāng)前存儲(chǔ)塊的可用存儲(chǔ)長(zhǎng)度去截取需要Append的字符串的長(zhǎng)度,放入到這個(gè)存儲(chǔ)塊的剩余位置,剩下的存儲(chǔ)不下的字符則存儲(chǔ)到擴(kuò)容的新的存儲(chǔ)塊m_ChunkChars中去,這個(gè)做法就是為了不浪費(fèi)存儲(chǔ)空間。
這一點(diǎn)考慮的非常周到,即使要發(fā)生擴(kuò)容,那么我當(dāng)前節(jié)點(diǎn)的存儲(chǔ)塊也一定要填充滿(mǎn),保證了存儲(chǔ)空間的最大利用。
通過(guò)上面的Append源碼我們自然可看出擴(kuò)容的邏輯自然也就在ExpandByABlock方法中[點(diǎn)擊查看源碼??]
//當(dāng)前StringBuilder實(shí)際存儲(chǔ)的總長(zhǎng)度
public int Length
{
//StringBuilder已存儲(chǔ)的長(zhǎng)度=塊的偏移量+當(dāng)前塊使用的長(zhǎng)度
get => m_ChunkOffset + m_ChunkLength;
set
{
//注意這里是有代碼的只是我們暫時(shí)省略set邏輯
}
}
//當(dāng)前StringBuilder的總?cè)萘?
public int Capacity
{
get => m_ChunkChars.Length + m_ChunkOffset;
set
{
//注意這里是有代碼的只是我們暫時(shí)省略set邏輯
}
}
//可承載字符的最大容量,即可以拼接的字符串的長(zhǎng)度
internal int m_MaxCapacity;
//承載【拼接字符串的char數(shù)組
internal char[] m_ChunkChars;
//當(dāng)前塊的最大長(zhǎng)度
internal const int MaxChunkSize = 8000;
//當(dāng)前m_ChunkChars字符數(shù)組中已經(jīng)使用的長(zhǎng)度
internal int m_ChunkLength;
//存儲(chǔ)塊的偏移量,用于計(jì)算總長(zhǎng)度
internal int m_ChunkOffset;
//前一個(gè)存儲(chǔ)塊
internal StringBuilder? m_ChunkPrevious;
private void ExpandByABlock(int minBlockCharCount)
{
//當(dāng)前塊m_ChunkChars存儲(chǔ)滿(mǎn)才進(jìn)行擴(kuò)容操作
Debug.Assert(Capacity == Length, nameof(ExpandByABlock) + " should only be called when there is no space left.");
//minBlockCharCount指的是剩下的需要存儲(chǔ)的長(zhǎng)度
Debug.Assert(minBlockCharCount > 0);
AssertInvariants();
//StringBuilder的總長(zhǎng)度不能大于StringBuilder的m_MaxCapacity
if ((minBlockCharCount + Length) > m_MaxCapacity || minBlockCharCount + Length < minBlockCharCount)
{
throw new ArgumentOutOfRangeException();
}
//!!!需要擴(kuò)容塊的新長(zhǎng)度=max(當(dāng)前追加字符的剩余長(zhǎng)度,min(當(dāng)前StringBuilder長(zhǎng)度,8000))
int newBlockLength = Math.Max(minBlockCharCount, Math.Min(Length, MaxChunkSize));
//判斷長(zhǎng)度是否越界
if (m_ChunkOffset + m_ChunkLength + newBlockLength < newBlockLength)
{
throw new OutOfMemoryException();
}
// 申請(qǐng)一個(gè)新的存塊長(zhǎng)度為newBlockLength
char[] chunkChars = GC.AllocateUninitializedArray<char>(newBlockLength);
//!!!把當(dāng)前StringBuilder中的存儲(chǔ)塊存放到一個(gè)新的StringBuilder實(shí)例中,當(dāng)前實(shí)例的m_ChunkPrevious指向上一個(gè)StringBuilder
//這里可以看出來(lái)擴(kuò)容的本質(zhì)是構(gòu)建節(jié)點(diǎn)為StringBuilder的鏈表
m_ChunkPrevious = new StringBuilder(this);
//偏移量是每次擴(kuò)容的時(shí)候去修改,它的長(zhǎng)度就是記錄了已使用塊的長(zhǎng)度,但是不包含當(dāng)前StringBuilder的存儲(chǔ)塊
//可以理解為偏移量=長(zhǎng)度-已經(jīng)存放擴(kuò)容塊的長(zhǎng)度
m_ChunkOffset += m_ChunkLength;
//因?yàn)橐呀?jīng)擴(kuò)容了新的容器所以重置已使用長(zhǎng)度
m_ChunkLength = 0;
//把新的塊重新賦值給當(dāng)前存儲(chǔ)塊m_ChunkChars數(shù)組
m_ChunkChars = chunkChars;
AssertInvariants();
}
這段代碼是擴(kuò)容的核心操作,通過(guò)這個(gè)我們可以清晰的了解到StringBuilder的存儲(chǔ)本質(zhì)
- 首先StringBuilder的數(shù)據(jù)存儲(chǔ)在m_ChunkChars字符數(shù)組中,但是擴(kuò)容本質(zhì)是單向鏈表操作,StringBuilder本身包含了m_ChunkPrevious指向的是上一個(gè)擴(kuò)容時(shí)保存的數(shù)據(jù)。
- 然后StringBuilder每次擴(kuò)容的長(zhǎng)度是不固定的,實(shí)際的擴(kuò)容長(zhǎng)度是max(當(dāng)前追加字符的剩余長(zhǎng)度,min(當(dāng)前StringBuilder長(zhǎng)度,8000)),由此我們可以以得知,一個(gè)塊m_ChunkChars數(shù)組的大小最大是8000。
StringBuilder還包含了一個(gè)通過(guò)StringBuilder構(gòu)建實(shí)例的方法,這個(gè)構(gòu)造函數(shù)就是給擴(kuò)容時(shí)候構(gòu)建單向鏈表使用的,它的實(shí)現(xiàn)也很簡(jiǎn)單
private StringBuilder(StringBuilder from)
{
m_ChunkLength = from.m_ChunkLength;
m_ChunkOffset = from.m_ChunkOffset;
m_ChunkChars = from.m_ChunkChars;
m_ChunkPrevious = from.m_ChunkPrevious;
m_MaxCapacity = from.m_MaxCapacity;
AssertInvariants();
}
其目的就是把擴(kuò)容之前的存儲(chǔ)相關(guān)的各種數(shù)據(jù)傳遞給新的StringBuilder實(shí)例。好了到目前為止Append的核心邏輯就說(shuō)完了,我們大致捋一下Append的核心邏輯我們先大致羅列一下,舉個(gè)例子
- 1.默認(rèn)情況m_ChunkChars[16],m_ChunkOffset=0,m_ChunkPrevious=null,Length=0
- 2.第一次擴(kuò)容m_ChunkChars[16],m_ChunkOffset=16,m_ChunkPrevious=指向最原始的StringBuilder,m_ChunkLength=16
- 3.第二次擴(kuò)容m_ChunkChars[32],m_ChunkOffset=32,m_ChunkPrevious=擴(kuò)容之前的m_ChunkChars[16]的StringBuilder,m_ChunkLength=32
- 4.第三次擴(kuò)容m_ChunkChars[64],m_ChunkOffset=64,m_ChunkPrevious=擴(kuò)容之前的m_ChunkChars[64]的StringBuilder,m_ChunkLength=64
大概花了一張圖,不知道能不能輔助理解一下StringBuilder的數(shù)據(jù)結(jié)構(gòu),StringBuilder的鏈表結(jié)構(gòu)是當(dāng)前節(jié)點(diǎn)指向上一個(gè)StringBuilder,即當(dāng)前擴(kuò)容之前的StringBuilder的實(shí)例

c# StringBuilder整體的數(shù)據(jù)結(jié)構(gòu)來(lái)說(shuō)是一個(gè)單向鏈表,但是鏈表的每一個(gè)節(jié)點(diǎn)存儲(chǔ)塊是m_ChunkChars是
char[]。擴(kuò)容的本質(zhì)就是給這個(gè)鏈表新增一個(gè)節(jié)點(diǎn),每次擴(kuò)容新增的節(jié)點(diǎn)存儲(chǔ)塊的容量都會(huì)增加。大部分使用時(shí)遇到的情況是首次為16、二次為16、三次為32、四次為64以此類(lèi)推。
轉(zhuǎn)換成字符串
通過(guò)上面StringBuilder的數(shù)據(jù)結(jié)構(gòu)我們了解到StringBuilder本質(zhì)的數(shù)據(jù)結(jié)構(gòu)是單向鏈表,這個(gè)單向鏈表包含m_ChunkPrevious指向上一個(gè)StringBuilder實(shí)例,也就是一個(gè)倒序的鏈表。我們最終拿到StringBuilder的構(gòu)建結(jié)果是通過(guò)StringBuilder的ToString()方法進(jìn)行的,得到最終的一個(gè)結(jié)果字符串,接下來(lái)我們就來(lái)看一下ToString的實(shí)現(xiàn)[點(diǎn)擊查看源碼??]
//當(dāng)前StringBuilder實(shí)際存儲(chǔ)的總長(zhǎng)度
public int Length
{
//StringBuilder已存儲(chǔ)的長(zhǎng)度=塊的偏移量+當(dāng)前塊使用的長(zhǎng)度
get => m_ChunkOffset + m_ChunkLength;
set
{
//注意這里是有代碼的只是我們暫時(shí)省略set邏輯
}
}
public override string ToString()
{
AssertInvariants();
//當(dāng)前StringBuilder長(zhǎng)度為0則直接返回空字符串
if (Length == 0)
{
return string.Empty;
}
//FastAllocateString函數(shù)負(fù)責(zé)分配長(zhǎng)度為StringBuilder長(zhǎng)度的字符串
//這個(gè)字符串就是ToString最終返回的結(jié)果,所以長(zhǎng)度等于StringBuilder的長(zhǎng)度
string result = string.FastAllocateString(Length);
//當(dāng)前StringBuilder是遍歷的第一個(gè)鏈表節(jié)點(diǎn)
StringBuilder? chunk = this;
do
{
//當(dāng)前使用長(zhǎng)度必須大于0,也就是說(shuō)當(dāng)前塊的m_ChunkChars必須使用過(guò),才需要遍歷當(dāng)前節(jié)點(diǎn)
if (chunk.m_ChunkLength > 0)
{
// 取出當(dāng)前遍歷的StringBuilder的相關(guān)數(shù)據(jù)
// 當(dāng)前遍歷StringBuilder的m_ChunkChars
char[] sourceArray = chunk.m_ChunkChars;
int chunkOffset = chunk.m_ChunkOffset;
int chunkLength = chunk.m_ChunkLength;
// 檢查是否越界
if ((uint)(chunkLength + chunkOffset) > (uint)result.Length || (uint)chunkLength > (uint)sourceArray.Length)
{
throw new ArgumentOutOfRangeException();
}
//把當(dāng)前遍歷項(xiàng)StringBuilder的m_ChunkChars逐步添加到result中當(dāng)前結(jié)果的前端
Buffer.Memmove(
ref Unsafe.Add(ref result.GetRawStringData(), chunkOffset),
ref MemoryMarshal.GetArrayDataReference(sourceArray),
(nuint)chunkLength);
}
//獲取當(dāng)前StringBuilder的前一個(gè)節(jié)點(diǎn),循環(huán)遍歷鏈表操作
chunk = chunk.m_ChunkPrevious;
}
//如果m_ChunkPrevious==null則代表是第一個(gè)節(jié)點(diǎn)
while (chunk != null);
return result;
}
關(guān)于這個(gè)ToString操作本質(zhì)就是一個(gè)倒序鏈表的遍歷操作,每一次遍歷都獲取當(dāng)前StringBuilder的m_ChunkPrevious字符數(shù)組獲取數(shù)據(jù)拼接完成之后,獲取當(dāng)前StringBuilder的上一個(gè)StringBuilder節(jié)點(diǎn),即m_ChunkPrevious的指向,結(jié)束的條件就是m_ChunkPrevious==null說(shuō)明該節(jié)點(diǎn)是首節(jié)點(diǎn),最終拼接成一個(gè)string字符串返回。關(guān)于這個(gè)執(zhí)行的遍歷過(guò)程大概可以理解為這么一個(gè)過(guò)程,比如咱們的StringBuilder里存放的是我和我的祖國(guó)一刻也不能分割,無(wú)論我走到哪里都留下一首贊歌。,那么針對(duì)ToString遍歷StringBuilder的遍歷過(guò)程則是大致如下的效果
//初始化一個(gè)等于StringBuilder長(zhǎng)度的字符串 string result = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; //第一次遍歷后 result = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0無(wú)論我走到哪里都留下一首贊歌。"; //第二次遍歷后 result = "\0\0\0\0\0\0\0一刻也不能分割,無(wú)論我走到哪里都留下一首贊歌。"; //第三次遍歷后 result = "\0\0\0我的祖國(guó)一刻也不能分割,無(wú)論我走到哪里都留下一首贊歌。"; //第三次遍歷后 result = "我和我的祖國(guó)一刻也不能分割,無(wú)論我走到哪里都留下一首贊歌。";
畢竟StringBuilder只能記錄上一個(gè)StringBuilder的數(shù)據(jù),因此這是一個(gè)倒序遍歷StringBuilder鏈表的操作,每次遍歷都是向前添加m_ChunkPrevious中記錄的數(shù)據(jù),直到m_ChunkPrevious==null則遍歷完成直接返回結(jié)果。
c# StringBuilder類(lèi)的ToString本質(zhì)就是倒序遍歷單向鏈表,鏈表的的每一個(gè)node都是StringBuilder實(shí)例,獲取里面的存儲(chǔ)塊
m_ChunkChars字符數(shù)組進(jìn)行拼裝,循環(huán)玩所有的節(jié)點(diǎn)之后把結(jié)果組裝成一個(gè)字符串返回。
對(duì)比java實(shí)現(xiàn)
我們可以看到在C#上StringBuilder的實(shí)現(xiàn),本質(zhì)是一個(gè)鏈表。那么和C#語(yǔ)言類(lèi)似的Java實(shí)現(xiàn)思路是否一致的,咱們大致看一下Java中StringBuilder的實(shí)現(xiàn)思路如何,我本地的jdk版本為1.8.0_191,首先也是初始化邏輯
//存儲(chǔ)塊也就是承載Append數(shù)據(jù)的容器
char[] value;
//StringBuilder的總長(zhǎng)度
int count;
public StringBuilder() {
//默認(rèn)的容量也是16
super(16);
}
public StringBuilder(String str) {
//這個(gè)地方有差異如果通過(guò)指定字符串初始化StringBuilder
//則初始化的長(zhǎng)度則是當(dāng)前傳遞的str的長(zhǎng)度+16
super(str.length() + 16);
append(str);
}
// AbstractStringBuilder.java
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
在這里可以看到j(luò)ava的初始化容量的邏輯和c#有點(diǎn)不同,c#默認(rèn)的初始化長(zhǎng)度取決于能存儲(chǔ)初始化字符串的長(zhǎng)度為主,而java的實(shí)現(xiàn)則是在當(dāng)前長(zhǎng)度上+16的長(zhǎng)度,也就是無(wú)論如何這個(gè)初始化的16的長(zhǎng)度必須要有。那么我們?cè)賮?lái)看一下append的實(shí)現(xiàn)源碼
// AbstractStringBuilder.java
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
// 這里是擴(kuò)容操作
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
//每次append之后重新設(shè)置長(zhǎng)度
count += len;
return this;
}
核心的是擴(kuò)容ensureCapacityInternal的方法,咱們簡(jiǎn)單的看下它的實(shí)現(xiàn)
private void ensureCapacityInternal(int minimumCapacity) {
//當(dāng)前需要的長(zhǎng)度>char[]的長(zhǎng)度則需要擴(kuò)容
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
//新擴(kuò)容的長(zhǎng)度是當(dāng)前塊char[]的長(zhǎng)度的2倍+2
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0)
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
//把當(dāng)前的char[]復(fù)制到新擴(kuò)容的字符數(shù)組中
value = Arrays.copyOf(value, newCapacity);
}
// Arrays.java copy的邏輯
public static char[] copyOf(char[] original, int newLength) {
//聲明一個(gè)新的數(shù)組,把original的數(shù)據(jù)copy到新的char數(shù)組中
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
最后要展示的則是得到StringBuilder結(jié)果的操作,同樣是toString方法,咱們看一下java中這個(gè)邏輯的實(shí)現(xiàn)
@Override
public String toString() {
// 這里創(chuàng)建了一個(gè)新的String對(duì)象返回,通過(guò)當(dāng)前char[]初始化這個(gè)字符串
return new String(value, 0, count);
}
到了這里關(guān)于java中StringBuilder的實(shí)現(xiàn)邏輯相信大家都看的非常清楚了,這里和c#的實(shí)現(xiàn)邏輯確實(shí)是不太一樣,本質(zhì)的底層數(shù)據(jù)結(jié)構(gòu)都是不一樣的,這里咱們簡(jiǎn)單的羅列一下它們實(shí)現(xiàn)方式的不同
- c#中StringBuilder的雖然真正數(shù)據(jù)存儲(chǔ)在
m_ChunkChars字符數(shù)組,但整體的數(shù)據(jù)結(jié)構(gòu)是單向鏈表,java中則完全是char[]字符數(shù)組。 - c#中StringBuilder的初始長(zhǎng)度是可容納當(dāng)前初始化字符串的長(zhǎng)度,java的初始化長(zhǎng)度則是當(dāng)前傳遞的字符串長(zhǎng)度+16。
- c#中StringBuilder的擴(kuò)容是生成一個(gè)新的StringBuilder實(shí)例,容量和上一個(gè)StringBuilder長(zhǎng)度有關(guān)。java則是生成一個(gè)是原來(lái)
char[]數(shù)組長(zhǎng)度*2+2長(zhǎng)度的新數(shù)組。 - c#中ToString的實(shí)現(xiàn)是遍歷倒序鏈表組裝一個(gè)新的字符串返回,java上則是用當(dāng)前StringBuilder的
char[]初始化一個(gè)新的字符串返回。
關(guān)于c#和java的StringBuilder實(shí)現(xiàn)方式差異如此之大,到底哪種實(shí)現(xiàn)方式更優(yōu)一點(diǎn)呢?這個(gè)沒(méi)辦法評(píng)價(jià),畢竟每一門(mén)語(yǔ)言的底層類(lèi)庫(kù)實(shí)現(xiàn)都是經(jīng)過(guò)深思熟慮的,集成了很多人的思想。在樓主的角度來(lái)看StringBuilder本身的核心功能在于構(gòu)建的過(guò)程,所以構(gòu)建過(guò)程的性能非常重要,所以類(lèi)似數(shù)組擴(kuò)容再copy的邏輯沒(méi)有鏈表的方式高效。但是在最后的ToString得到結(jié)果的時(shí)候,數(shù)組的優(yōu)勢(shì)是非常明顯的,畢竟string本質(zhì)就是一個(gè)char[]數(shù)組。
對(duì)于StringBuilder來(lái)說(shuō)append是頻繁操作大部分情況可能多次進(jìn)行append操作,而ToString操作對(duì)于StringBuilder來(lái)說(shuō)基本上只有一次,那就是得到StringBuilder構(gòu)建結(jié)果的時(shí)候。所以樓主覺(jué)得提升append的性能是關(guān)鍵。
總結(jié)
本文我們主要講解了c# StringBuilder的大致的實(shí)現(xiàn)方式,同時(shí)也對(duì)比了c#和java關(guān)于實(shí)現(xiàn)方式的StringBuilder的不同,主要差異是c#實(shí)現(xiàn)的底層數(shù)據(jù)結(jié)構(gòu)為單向鏈表,但是每一個(gè)節(jié)點(diǎn)的數(shù)據(jù)存儲(chǔ)在char[]中,java實(shí)現(xiàn)的方式則整體都是數(shù)組。這也為我們提供了不同的思路,在這里我們也再次總結(jié)一下它的實(shí)現(xiàn)方式
- c# StringBuilder的本質(zhì)是單向鏈表操作,StringBuilder本身包含了
m_ChunkPrevious指向的是上一個(gè)擴(kuò)容時(shí)保存的數(shù)據(jù),擴(kuò)容的本質(zhì)就是給這個(gè)鏈表新增一個(gè)節(jié)點(diǎn)。 - c# StringBuilder每次擴(kuò)容的長(zhǎng)度是不固定的,實(shí)際的擴(kuò)容長(zhǎng)度是
max(當(dāng)前追加字符的剩余長(zhǎng)度,min(當(dāng)前StringBuilder長(zhǎng)度,8000)),每次擴(kuò)容新增的節(jié)點(diǎn)存儲(chǔ)塊的容量都會(huì)增加。大部分使用時(shí)遇到的情況是首次為16、二次為16、三次為32、四次為64以此類(lèi)推。 - c# StringBuilder類(lèi)的ToString本質(zhì)就是倒序遍歷單向鏈表,每一次遍歷都獲取當(dāng)前StringBuilder的
m_ChunkPrevious字符數(shù)組獲取數(shù)據(jù)拼接完成之后,然后獲取m_ChunkPrevious指向的上一個(gè)StringBuilder實(shí)例,最終把結(jié)果組裝成一個(gè)字符串返回。 - 關(guān)于c#和java實(shí)現(xiàn)StringBuilder存在很大差異,主要差異是c#實(shí)現(xiàn)的整體底層數(shù)據(jù)結(jié)構(gòu)為單向鏈表,但是每個(gè)StringBuilder實(shí)例中數(shù)據(jù)本身存儲(chǔ)在
char[]中,這種數(shù)據(jù)結(jié)構(gòu)有點(diǎn)像redis的quicklist。java實(shí)現(xiàn)的整體方式則都是char[]字符數(shù)組。
雖然大家都說(shuō)越努力越幸運(yùn),有時(shí)候我們努力是為了讓自己更幸運(yùn)。但是我更喜歡的是,我們努力不僅僅是為了幸運(yùn),而是讓我們的心里更踏實(shí),結(jié)果固然重要,然而許多時(shí)候努力過(guò)了也就問(wèn)心無(wú)愧了。
到此這篇關(guān)于C#字符串構(gòu)建利器StringBuilder的文章就介紹到這了,更多相關(guān)C#字符串構(gòu)建利器StringBuilder內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#實(shí)現(xiàn)HSL顏色值轉(zhuǎn)換為RGB的方法
這篇文章主要介紹了C#實(shí)現(xiàn)HSL顏色值轉(zhuǎn)換為RGB的方法,涉及C#數(shù)值判定與轉(zhuǎn)換的相關(guān)技巧,需要的朋友可以參考下2015-06-06
C#使用stackalloc分配堆棧內(nèi)存和非托管類(lèi)型詳解
這篇文章主要為大家介紹了C#使用stackalloc分配堆棧內(nèi)存和非托管類(lèi)型詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-12-12
基于mvc5+ef6+Bootstrap框架實(shí)現(xiàn)身份驗(yàn)證和權(quán)限管理
最近剛做完一個(gè)項(xiàng)目,項(xiàng)目架構(gòu)師使用mvc5+ef6+Bootstrap,用的是vs2015,數(shù)據(jù)庫(kù)是sql server2014。下面小編把mvc5+ef6+Bootstrap項(xiàng)目心得之身份驗(yàn)證和權(quán)限管理模塊的實(shí)現(xiàn)思路分享給大家,需要的朋友可以參考下2016-06-06
Unity中Instantiate實(shí)例化物體卡頓問(wèn)題的解決
這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)離線(xiàn)計(jì)時(shí)器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10
C#實(shí)現(xiàn)兩個(gè)richtextbox控件滾動(dòng)條同步滾動(dòng)的簡(jiǎn)單方法
這篇文章主要給大家介紹了C#實(shí)現(xiàn)兩個(gè)richtextbox控件滾動(dòng)條同步滾動(dòng)的簡(jiǎn)單方法,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-05-05
C#實(shí)現(xiàn)用戶(hù)自定義控件中嵌入自己的圖標(biāo)
這篇文章主要介紹了C#實(shí)現(xiàn)用戶(hù)自定義控件中嵌入自己的圖標(biāo),較為詳細(xì)的分析了C#實(shí)現(xiàn)自定義控件中嵌入圖標(biāo)的具體步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-03-03

