在拼接字符串时,应该使用StringBuilder还是String?
摘要:字符串拼接这事,看起来小,但用错了地方,真能把程序拖垮。别再凭感觉了,记住三个关键词:少量用“+”、循环用Builder、集合用Join。写出性能好的代码,从选对拼接方式开始。
问题:拼接字符串,到底用哪个?
先问个实在的问题:你在代码里怎么拼接字符串?
很多兄弟可能是这么写的:
stringstr ="Hello"+" "+"World"; //当然这里只是举个例子
也有的会在循环里这么干:
stringresult ="";for(inti =0; i <1000; i++){ result += i.ToString();// 循环拼接}
然后有一天,你听说了StringBuilder,据说拼接性能更好。于是你开始纠结:到底该用“+”还是StringBuilder?网上说法五花八门,有的说“+”慢成狗,有的说编译器会优化,根本不用操心。
今天咱们就把这事掰扯清楚,以后别再凭感觉写了。
结论:看场景,别迷信
一句话总结:
少量、固定次数的拼接,直接用“+”,代码简洁,编译器还会帮你优化。
大量、循环内的拼接,尤其是次数不确定时,务必用StringBuilder,否则性能可能崩盘。
特殊场景(如拼接集合、格式化)考虑用string.Concat、string.Join或字符串插值,它们底层已经做了优化。
下面展开聊聊为什么。
扩展:从内存到原理,一次讲透
1. 字符串的“不可变性”是罪魁祸首
C#里的字符串是引用类型,而且是不可变的。什么意思?就是你一旦创建了一个字符串,它就定型了,内容不能再改。当你试图“修改”它时,其实是创建了一个全新的字符串对象,原来的那个等着被垃圾回收。
比如:
strings ="Hello";s= s +" World";
第二行执行时,先在内存里创建一个新字符串"Hello World",然后把s指向它,原来的"Hello"就成了没人要的孤儿,等着GC来收。
这种设计有好处(线程安全、哈希缓存等),但拼接时就成了性能杀手。
2. “+”拼接的真相:分情况讨论
情况A:编译期就能确定的拼接
stringstr ="Hello"+" "+"World";
这种代码在编译时,编译器直接把它优化成了"Hello World",生成的IL里只有一个字符串。所以运行时没有任何拼接开销,放心用。
情况B:拼接中包含变量
stringname ="刚子";stringmsg ="Hello, "+ name +"!";
这种编译器会把它转成string.Concat调用,比如:
stringmsg =string.Concat("Hello, ", name,"!");
string.Concat内部会根据参数数量,一次性计算出最终字符串长度,然后分配内存,把各部分拷贝进去。一次拼接,一次分配,效率其实不错。所以这种少量“+”拼接,完全没问题。
情况C:循环内反复拼接
stringresult ="";for(inti =0; i <10000; i++){ result += i.ToString();}
这里每循环一次,都会产生一个新的字符串,而且一次比一次大。比如:
第1次:长度1,分配1次
第2次:长度2,新分配,拷贝之前的结果和新的字符
第3次:长度3,再分配,拷贝……
这样总共分配了10000次字符串,拷贝的总字符数大约是1+2+...+10000 ≈ 5000万次!时间复杂度O(n²),数据量大时直接卡死。
3. StringBuilder 为什么快?
StringBuilder内部维护了一个可变的字符数组(char[])。当你 Append 时,它会直接往数组里写,空间不够了就自动扩容(通常是翻倍)。所有追加操作都在同一个数组里进行,只有最后调用ToString()时才真正创建一次字符串。
所以上面的循环用StringBuilder改写:
StringBuildersb=newStringBuilder();for(inti=0; i <10000; i++){ sb.Append(i.ToString());}stringresult=sb.ToString();
扩容次数:大约 log₂(10000) ≈ 14次(假设初始容量16)
字符拷贝总量:约10000次,远小于“+”的5000万次
时间复杂度O(n),性能天壤之别。
4. 来点实测数据
我写了个简单测试(环境:.NET 6, Release模式,各执行10万次拼接):
方式
10次拼接
1000次拼接
10万次拼接
+(循环内)
<1ms
15ms
爆炸(几秒)
StringBuilder
<1ms
<1ms
8ms
注意,“+”在小数量时差距不大,但一旦数量上去,直接崩盘。
