型別 · 字串
C# 裡 string 與 StringBuilder
解釋一切的那句話:C# 字串不可變——沒有任何字串方法會修改字串,它們全都返回一個新的。StringBuilder 的存在就是為了這個事實。
不可變性,演示出來
執行這段,看一個方法呼叫什麼都沒幹:
string s = "hello";
s.Replace("l", "L"); // 返回一個新字串——s 紋絲不動
Console.WriteLine(s);
string s = "hello";
s = s.Replace("l", "L"); // 把新字串接回來
Console.WriteLine(s); // heLLo
s = s.ToUpper(); // 每個字串方法都是同一條規則
Console.WriteLine(s); // HELLO
// 原始字面量從未改變——被製造出來的是新字串:
Console.WriteLine("original still intact: " + ("hello" == "hello"));
想通之後,一整個 bug 家族就消失了:"為什麼我的 Replace 沒生效?"——因為返回值被你丟了。字串是恰好裝著文本的值;把每個方法的返回值當成真正的產出。
為什麼要不可變?
- 共享安全。把字串傳給任何方法,對方都改不了"你那份"——因為根本沒有可改的東西。(類正相反:引用別名。)
- 免費駐留。相同的字面量可以安全共享同一份儲存——這就是為什麼
"apple" == "apple"哪怕按引用比較也能是 True。 - 可雜湊。不可變的鍵雜湊值不會漂移——這正是字串能當
Dictionary鍵的原因。
代價:在迴圈裡拼字串
string msg = "";
for (int i = 1; i <= 5; i++)
{
msg += i + ","; // 每次 += 都造一個全新字串:舊的 + 新片段
}
Console.WriteLine(msg);
五次迴圈,無所謂。算筆賬:第 n 輪要複製前 n−1 輪攢下的全部字元才能造出新串。總複製量按 1+2+3+…+n ≈ n²/2 個字元增長。n 到十萬就是幾十億次字元複製,外加一座垃圾山。StringBuilder 就是為修這個而生的。
StringBuilder:可變的緩衝區
StringBuilder 維護內部緩衝區,直接往裡追加——每次 append 不造新串、不複製舊內容。只在最後 ToString() 那一刻付一次賬:
// StringBuilder 不在瀏覽器引擎的教學子集裡——在 Visual Studio 裡原樣可跑
using System.Text;
var sb = new StringBuilder();
for (int i = 1; i <= 100000; i++)
{
sb.Append(i);
sb.Append(',');
}
string msg = sb.ToString(); // 只在最後造一次字串
在引擎裡把迴圈版跑到 n = 100000 感受一下差距;然後相信 StringBuilder 眨眼間就能幹完同樣的活。(引擎裡的等價物是 List<T> + string.Join:var parts = new List<string>(); …… string.Join(",", parts)。)
那麼到底用哪個?
| 場景 | 用 | 為什麼 |
|---|---|---|
兩三個片段、拼一次(name + " is " + age) | + / 插值 | 可讀;反正只有一次分配 |
| 迴圈裡攢文本 | StringBuilder | O(n) 而不是 O(n²) |
| 拼接集合 | string.Join | 專用工具,一次呼叫完事 |
| 不可變的文本值 | string | 它就是預設——而且是安全網 |
經驗法則:片段少 → 插值;迴圈 → StringBuilder;集合 → Join。別給兩三個片段的情況提前最佳化——$"{name} is {age}" 寫得最快,跑得也夠快。
一分鐘自測
兩行都有輸出——去練習場執行前先各預測一次:
string s = "cat";
s.Replace("c", "b");
Console.WriteLine(s); // "bat" 還是 "cat"?(不可變性)
s = s.Replace("c", "b");
Console.WriteLine(s); // 那現在呢?