正確操作C#字符串
正確操作C#字符串
本文是《編寫高質量代碼改善C#程序的157個建議》第一章基本語言要素之建議1正確操作字符串,喜歡本書請到各大商城購買原書,支持正版。
?第一章序
?如何操作字符串?如何進行轉型?什么是克隆?什么是相等型?為什么需要HashCode?當我們真正開始使用一門語言進行編程時,就會遇到這些問題。這些問題看起來簡單,可是我們是否想過:為什么要這樣處理,這樣做是最好的嗎?本章的內容將以這些最基礎的要素開篇,給出C#編碼中的一些最佳實踐,讓我們在C#進階學習的過程中始終朝著正確的方向前進。
建議1:正確操作字符串
字符串應該是所有編程語言中使用最頻繁的一種基礎數據類型。如果使用不慎,我們就會為一次字符串的操作所帶來的額外性能開銷而付出代價。本條建議將從兩個方面來探討如何規(guī)避這類性能開銷:
- 確保盡量少的裝箱
- 避免分配額外的內存空間
先來介紹第一個方面,請看下面的兩行代碼:
String?str1?=?"str1"?+?9;
String?str2?=?"str2"?+?9.ToString();
為了清楚這行代碼的執(zhí)行情況,我們來比較兩者生成的IL代碼。
這里先說明下怎么看IL代碼,應該有人不知道怎么看:
查看IL代碼
- 打開vs 2019命令行
開始菜單打開vs 2019命令行,截圖如下:
打開命令行- 打開IL DASM窗口
打開IL DASM窗口- 查看IL代碼
這里有兩種方式,一是菜單打開目標程序:文件=》打開,二是直接將文件拖入IL DASM窗口

將上面的IL代碼貼出來分析哈。
.method?private?hidebysig?static?void??Main(string[]?args)?cil?managed
{
??.entrypoint
??//?代碼大小???????44?(0x2c)
??.maxstack??2
??.locals?init?(string?V_0,
???????????string?V_1,
???????????int32?V_2)
??IL_0000:??nop
??IL_0001:??ldstr??????"str1"
??IL_0006:??ldc.i4.s???9
??IL_0008:??stloc.2
??IL_0009:??ldloca.s???V_2
??IL_000b:??call???????instance?string?[System.Runtime]System.Int32::ToString()
??IL_0010:??call???????string?[System.Runtime]System.String::Concat(string,
????????????????????????????????????????????????????????????????????string)
??IL_0015:??stloc.0
??IL_0016:??ldstr??????"str2"
??IL_001b:??ldc.i4.s???9
??IL_001d:??stloc.2
??IL_001e:??ldloca.s???V_2
??IL_0020:??call???????instance?string?[System.Runtime]System.Int32::ToString()
??IL_0025:??call???????string?[System.Runtime]System.String::Concat(string,
????????????????????????????????????????????????????????????????????string)
??IL_002a:??stloc.1
??IL_002b:??ret
}?//?end?of?method?Program::Main
兩行代碼生成的IL代碼幾乎一模一樣,我是用.NET 5生成了,我們使用.NET Framework生成試試:
.NET Framework 2.0|3.0|3.5|4.0|4.5
.method?private?hidebysig?static?void??Main(string[]?args)?cil?managed
{
??.entrypoint
??//?代碼大小???????44?(0x2c)
??.maxstack??2
??.locals?init?([0]?string?str1,
???????????[1]?string?str2,
???????????[2]?int32?V_2)
??IL_0000:??nop
??IL_0001:??ldstr??????"str1"
??IL_0006:??ldc.i4.s???9
??IL_0008:??stloc.2
??IL_0009:??ldloca.s???V_2
??IL_000b:??call???????instance?string?[mscorlib]System.Int32::ToString()
??IL_0010:??call???????string?[mscorlib]System.String::Concat(string,
??????????????????????????????????????????????????????????????string)
??IL_0015:??stloc.0
??IL_0016:??ldstr??????"str2"
??IL_001b:??ldc.i4.s???9
??IL_001d:??stloc.2
??IL_001e:??ldloca.s???V_2
??IL_0020:??call???????instance?string?[mscorlib]System.Int32::ToString()
??IL_0025:??call???????string?[mscorlib]System.String::Concat(string,
??????????????????????????????????????????????????????????????string)
??IL_002a:??stloc.1
??IL_002b:??ret
}?//?end?of?method?Program::Main
也是一樣,怎么回事?和書上不一致呢?
原書IL代碼(第2頁末、第3頁)
原書IL代碼對比原書IL代碼說明(第3頁)
書中說明什么原因造成自己的IL代碼和書中不同呢?
本號主猜測,未驗證:
?號主使用的vs 2019編譯的程序,編譯器可能對程序做了足夠的優(yōu)化;而原書是10年出版,當時應該是vs 2010(或者vs 2008)編譯的,舊編譯器優(yōu)化工作沒有2021年現在的編譯器這么優(yōu)秀...
這個建議我們暫時跳過,但本建議最后原作者推薦大家使用StringBuilder,號主非常贊同:
?
StringBuilder并不會重新創(chuàng)建一個string對象,它的效率源于預先以非托管的方式分配內存。如果StringBuilder沒有先定義長度,則默認分配的長度為16。當StringBuilder字符長度小于等于16時,StringBuilder不會重新分配內存;當StringBuilder字符長度大于16小于32時,StringBuilder又會重新分配內存,使之成為16的倍數。如果預先判斷字符串的長度將大于16,則可以為其設定一個更加合適的長度(如32)。StringBuilder重新分配內存時是按照上次的容量加倍進行分配的。當然,我們需要注意,StringBuilder指定的長度要合適,太小了,需要頻繁分配內存;太大了,浪費空間。
微軟還提供了另外一個方法來簡化StringBuilder的操作,即使用string.Format方法。string.Format方法在內部使用StringBuilder進行字符串的格式化,如下面的代碼所示:
private?static?void?NewMethod11()
{
??//?為了演示的需要,定義了4個變量
??string?a?=?"t";
??string?b?=?"e";
??string?c?=?"s";
??string?d?=?"t";
??string?e?=?string.Format("{0}{1}{2}{3}",?a,?b,?c,?d);
}
當然現在還有更簡化的方式:string e = $"{a}{b}{c}go7utgvlrp"。
下一篇,我們讀建議2:使用默認轉型方法。
