在 C# 圈子裡,「字串連接用 StringBuilder,String 效能差」幾乎成了信條。無數 Benchmark 證明 String += 在迴圈中會產生大量暫時物件,導致 GC 壓力劇增。於是我們開始濫用 StringBuilder,甚至有人把它封裝成「增強版字串類別」來取代 String。但所有字串操作 StringBuilder 真的都比 String 快嗎?
| 特性 | String | StringBuilder |
|---|---|---|
| 本質 | 不可變參考型別(語義上像值型別) | 可變參考型別,內部維護字元緩衝區 |
| 修改行為 | 每次修改產生新實例 | 就地修改緩衝區,容量不足時擴容 |
| 方法鏈結 | 需重新指派:s = s.Replace(...) | 直接呼叫:sb.Replace(...) |
| 執行緒安全 | 天生安全(不可變) | 非安全,需自行同步 |
筆者在 VS2008 環境下,針對常見操作進行微基準測試(10 萬次迴圈,釋放版最佳化):
// 測試 1:字串連接
String s = "";
for (int i = 0; i < 100000; i++) s += "x"; // ~1,200 ms
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) sb.Append("x"); // ~2 ms
// 測試 2:單次 Replace
String s2 = "abcdefghijklmnopqrstuvwxyz";
for (int i = 0; i < 100000; i++) s2 = s2.Replace("a", "z"); // ~45 ms
StringBuilder sb2 = new StringBuilder("abcdefghijklmnopqrstuvwxyz");
for (int i = 0; i < 100000; i++) sb2.Replace('a', 'z'); // ~38 ms
// 測試 3:Substring / 索引存取
String s3 = "0123456789";
for (int i = 0; i < 1000000; i++) { var c = s3[5]; } // ~12 ms
StringBuilder sb3 = new StringBuilder("0123456789");
for (int i = 0; i < 1000000; i++) { var c = sb3[5]; } // ~18 ms
StringBuilder 完勝,差距可達百倍。StringBuilder 仍略勝一籌。String 反而更快,因為無邊界檢查與緩衝區間接存取開銷。Substring、IndexOf、Split、Contains、索引器存取。Insert/Remove/Replace 且資料量大。new StringBuilder(capacity) 避免多次擴容。.NET Core 2.1+ 引入 System.Memory,提供 String.Create 與 Span<char>,可在不分配中間字串的情況下直接寫入最終字串:
string result = String.Create(100, (buffer, state) => {
// 直接在 buffer (Span<char>) 填寫內容
for (int i = 0; i < 100; i++) buffer[i] = (char)('A' + i % 26);
}, null);
這是「零分配」字串構建的現代解法,效能優於 StringBuilder,且回傳不可變 String,兼具安全與速度。
StringBuilder 不是銀彈。理解「不可變語意」與「緩衝區擴容機制」的本質差異,依據操作模式(寫多讀少 vs 讀多寫少)與資料規模選擇工具,才是高效 C# 開發者的必修課。下次寫 sb.Append 前,先問自己:「這裡真的是寫入熱點嗎?」