程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#中如何正確的操作字符串?,

C#中如何正確的操作字符串?,

編輯:C#入門知識

C#中如何正確的操作字符串?,


字符串應該是所有編程語言中使用最頻繁的一種基礎數據類型。如果使用不慎,我們就會為一次字符串的操作所帶來的額外性能開銷而付出代價。本條建議將從兩個方面來探討如何規避這類性能開銷:
1. 確保盡量少的裝箱
2. 避免分配額外的內存空間。

第一個方面:確保盡量少的裝箱

對於裝拆箱,我們應該不陌生,值類型轉換成引用類型即為裝箱, 引用類型轉換成值類型即為拆箱。 在自己編寫的代碼中,應當盡可能的避免編寫不必要的裝箱代碼。裝箱之所以會帶來性能損耗,因為它需要完成下面三個步驟:
• 首先,會為值類型在托管堆中分配內存。除了值類型本身所分配的內存外,內存總量還要加上類型對象指針和同步塊索引所占用的內存。
• 然後,將值類型的值復制到新分配的堆內存中。
• 最後,返回已經成為引用類型的對象的地址。

下面是一行最簡單的裝箱代碼

1 object obj = 1;

這行語句將整型常量1賦給object類型的變量obj; 眾所周知常量1是值類型,值類型是要放在棧上的,而object是引用類型,它需要放在堆上;要把值類型放在堆上就需要執行一次裝箱操作。

這行語句的IL代碼如下,請注意注釋部分說明:

.locals init (
[0] object objValue
) //以上三行IL表示聲明object類型的名稱為objValue的局部變量 
IL_0000: nop
IL_0001: ldc.i4.s 9 //表示將整型數9放到棧頂
IL_0003: box [mscorlib]System.Int32 //執行IL box指令,在內存堆中申請System.Int32類型需要的堆空間
IL_0008: stloc.0 //彈出堆棧上的變量,將它存儲到索引為0的局部變量中

以上就是裝箱所要執行的操作了,執行裝箱操作時不可避免的要在堆上申請內存空間,並將堆棧上的值類型數據復制到申請的堆內存空間上,這肯定是要消耗內存和cpu資源的。我們再看下拆箱操作是怎麼回事:

請看下面的C#代碼:

object objValue = 4;
int value = (int)objValue;

上面的兩行代碼會執行一次裝箱操作將整形數字常量4裝箱成引用類型object變量objValue;然後又執行一次拆箱操作,將存儲到堆上的引用變量objValue存儲到局部整形值類型變量value中。

同樣我們需要看下IL代碼:

.locals init (
[0] object objValue,
[1] int32 'value'
) //上面IL聲明兩個局部變量object類型的objValue和int32類型的value變量
IL_0000: nop
IL_0001: ldc.i4.4 //將整型數字4壓入棧
IL_0002: box [mscorlib]System.Int32 //執行IL box指令,在內存堆中申請System.Int32類型需要的堆空間
IL_0007: stloc.0 //彈出堆棧上的變量,將它存儲到索引為0的局部變量中
IL_0008: ldloc.0//將索引為0的局部變量(即objValue變量)壓入棧
IL_0009: unbox.any [mscorlib]System.Int32 //執行IL 拆箱指令unbox.any 將引用類型object轉換成System.Int32類型
IL_000e: stloc.1 //將棧上的數據存儲到索引為1的局部變量即value

拆箱操作的執行過程和裝箱操作過程正好相反,是將存儲在堆上的引用類型值轉換為值類型並給值類型變量。

裝箱操作和拆箱操作是要額外耗費CPU和內存資源的。那如何避免裝箱和拆箱操作呢?有以下方法:
1. 用泛型集合取代ArrayList。
2. 用C#自帶的轉換方法,將值類型轉換為引用類型。

下面我們看下使用泛型和不使用泛型引發裝箱拆箱的情況。
1. 使用非泛型集合時引發的裝箱和拆箱操作

看下面的一段代碼:

var array = new ArrayList();
array.Add(1);
array.Add(2);

foreach (int value in array)
{
Console.WriteLine(“value is {0}”,value);
}

代碼聲明了一個ArrayList對象,向ArrayList中添加兩個數字1,2;然後使用foreach將ArrayList中的元素打印到控制台。

在這個過程中會發生兩次裝箱操作和兩次拆箱操作,在向ArrayList中添加int類型元素時會發生裝箱,在使用foreach枚舉ArrayList中的int類型元素時會發生拆箱操作,將object類型轉換成int類型,在執行到Console.WriteLine時,還會執行兩次的裝箱操作;這一段代碼執行了6次的裝箱和拆箱操作;如果ArrayList的元素個數很多,執行裝箱拆箱的操作會更多。

你可以通過使用ILSpy之類的工具查看IL代碼的box,unbox指令查看裝箱和拆箱的過程

2. 使用泛型集合的情況

請看如下代碼:

1 var list = new List<int>();
2 list.Add(1);
3 list.Add(2);
4 
5 foreach (int value in list)
6 {
7 Console.WriteLine("value is {0}", value);
8 }

 

代碼和1中的代碼的差別在於集合的類型使用了泛型的List,而非ArrayList;我們同樣可以通過查看IL代碼查看裝箱拆箱的情況,上述代碼只會在Console.WriteLine()方法時執行2次裝箱操作,不需要拆箱操作。

可以看出泛型可以避免裝箱拆箱帶來的不必要的性能消耗;當然泛型的好處不止於此,泛型還可以增加程序的可讀性,使程序更容易被復用等等。

但是我們注意到,在使用泛型集合的時候,Console.WriteLine()方法時仍然執行2次裝箱操作。能否將這兩次裝箱操作也優化掉呢?這就使用到了第二個方法,用C#自帶的轉換方法,將值類型轉換為引用類型。如下:

var list = new List<int>();
list.Add(1);
list.Add(2);

foreach (int value in list)
{
Console.WriteLine(string.Format("value is {0}", value.ToString()));
}

再查看IL代碼時可以發現,裝箱操作已經被徹底消除了。它實際調用的是整形的ToString方法。ToString方法的原型為:

public override string ToString()
{
return Number.FormatInt32(m_value, null, NumberFormatInfo.CurrentInfo);
}

它是通過直接操作內存來完成從int到string的轉換,效率要比裝箱高很多。所以,在使用其他值類型到字符串的轉換並完成拼接時,應當避免使用操作符“+”來完成,而應該使用值類型提供的ToString方法。

第二個方面:避免分配額外的內存空間。
對CLR來說,string對象是個很特殊的對象,它一旦被賦值就不可改變。在運行時調用System.String類中的任何方法或進行任何運算(如“=”賦值,“+”拼接等),都會在內存中創建一個新的字符串對象,這也意味著要為該新對象分配新的內存空間。像下面的代碼就會帶來運行時的額外開銷。

private static void Test6()
{
string s1 = "abc";
s1 = "123" + s1 + "456"; // 以上兩行代碼創建了3個String對象,並執行了一次String.Contact方法
string s2 = 9 + "456"; // 該代碼發生一次裝箱,並調用一次String.Concact方法
}

private static void Test7()
{
string s1 = "123" + "abc" + "456"; // 該代碼等效於string s1 = "123abc456"
}    

由於使用String類會在某些場合帶來明顯的性能損耗,所以微軟另外提供了一個類型StringBuilder來彌補String的不足。

StringBuilder並不會重新創建一個String對象,它的效率源於預先以非托管的方式分配內存。如果StringBuilder沒有先定義長度,則默認分配的長度為16,當StringBuilder字符長度小於等於16時,StringBuilder不會重新分配內存。當StringBuilder字符長度大於16時小於32時,StringBuilder又會重新分配內存,使之成為16的倍數。在上面的代碼中,如果預先判斷字符串的長度將大於16,則可以為其設定一個更加合適的長度。
微軟還提供了另外一個方法來簡化這種操作,即使用string.Format方法。string.Format方法在內部使用StringBuilder進行字符串的格式化。

private static void Test9()
{
string a = "t";
string b = "e";
string c = "s";
string d = "t";

StringBuilder sb = new StringBuilder();
sb.Append(a);
sb.Append(b);
sb.Append(c);
sb.Append(d);

Console.WriteLine(sb.ToString());
}

private static void Test10()
{
string a = "t";
string b = "e";
string c = "s";
string d = "t";

Console.WriteLine(string.Format("{0}{1}{2}{3}", a, b, c, d));
}

最後總結:如何正確操作字符串:

1. 確保盡量少的拆裝箱操作:使用泛型,使用ToString()將值類型轉換為引用類型
2. 避免分配額外的內存空間:不用+=, +操作符, 使用StringBuilder, String.Format()鏈接多個String

 

參考引用列表:​

http://www.cnblogs.com/yukaizhao/archive/2011/10/18/csharp_box_unbox_1.html
http://www.cnblogs.com/yukaizhao/archive/2011/10/19/csharp_box_unbox_2.html
​《編寫高質量代碼:改善C#程序的157個建議》

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved