前言 本文主要來學習記錄前三個建議。 建議1、正確操作字元串 建議2、使用預設轉型方法 建議3、區別對待強制轉換與as和is 其中有很多需要理解的東西,有些地方可能理解的不太到位,還望指正。 建議1、正確操作字元串 字元串應該是所有編程語言中使用最頻繁的一種基礎數據類型。如果使用不慎,我們就會為一次 ...
前言
本文主要來學習記錄前三個建議。
建議1、正確操作字元串
建議2、使用預設轉型方法
建議3、區別對待強制轉換與as和is
其中有很多需要理解的東西,有些地方可能理解的不太到位,還望指正。
建議1、正確操作字元串
字元串應該是所有編程語言中使用最頻繁的一種基礎數據類型。如果使用不慎,我們就會為一次字元串的操作所帶來的額外性能開銷而付出代價。本條建議將從兩個方面來探討如何規避這類性能開銷:
1、確保儘量少的裝箱
2、避免分配額外的記憶體空間
先來介紹第一個方面,請看下麵的兩行代碼:
String str1="str1"+9; String str2="str2"+9.ToString();
從IL代碼可以得知,第一行代碼在運行時完成一次裝箱的行為,而第二行代碼中並沒有發生裝箱的行為,它實際調用的是整型的ToString()方法,效率要比裝箱高。所以,在使用其他值引用類型到字元串的轉換並完成拼接時,應當避免使用操作符“+”來完成,而應該使用值引用類型提供的ToString()方法。
第二方面,避免分配額外的記憶體空間。對CLR來說,string對象(字元串對象)是個很特殊的對象,它一旦被賦值就不可改變。在運行時調用System.String類中的任何方法或進行任何運算(如“=”賦值、“+”拼接等),都會在記憶體中創建一個新的字元串對象,這也意味著要為該新對象分配新的記憶體空間。像下麵的代碼就會帶來運行時的額外開銷。
private static void NewMethod1() { string s1="abc"; s1="123"+s1+"456"; ////以上兩行代碼創建了3個字元串對象對象,並執行了一次string.Contact方法 } private static void NewMethod2() { string re=9+"456"; ////該方法發生了一次裝箱,並調用一次string.Contact方法 }
關於裝箱拆箱的問題大家可以查看我之前的文章http://www.cnblogs.com/aehyok/p/3504449.html
而以下代碼,字元串不會在運行時進行拼接,而是會在編譯時直接生成一個字元串。
private static void NewMethod3() { string re2="123"+"abc"+"456"; ///該代碼等效於///string re2="123abc456"; } private static void NewMethod4() { const string a="t"; string re="abc"+a; ///因為a是一個常量,所以該代碼等效於string=re="abc"+"t"; 最終等效於string re="abct"; }
由於使用System.String類會在某些場合帶來明顯的性能損耗,所以微軟另外提供了一個類型StringBuilder來彌補String的不足。
StringBuilder並不會重新創建一個string對象,它的效率源於預先以非托管的方式分配記憶體。如果StringBuilder沒有先定義長度,則預設分配的長度為16。當StringBuilder字元串長度小於等於16時,StringBuilder不會重新分配記憶體;當StringBuilder字元長度大於16小於32時,StringBuilder又會重新分配記憶體,使之成為16的倍數。在上面的代碼中,如果預先判斷字元串的長度將大於16,則可以為其設定一個更加合適的長度(如32)。StringBuilder重新分配記憶體時是按照上次容量加倍進行分配的。當然,我們需要註意,StringBuilder指定的長度要合適,太小了,需要頻繁分配記憶體,太大了,浪費空間。
查看以下代碼,比較下麵兩種字元串拼接方式,哪種效率更高:
private static void NewMethod1() { string a = "t"; a += "e"; a += "s"; a += "t"; } private static void NewMethod2() { string a = "t"; string b = "e"; string c = "s"; string d = "t"; string result = a + b + c + d; }
結果可以得知:兩者的效率都不高。不要以為前者比後者創建的字元串對象更少,事實上,兩者創建的字元串對象相等,且前者進行了3次string.Contact方法調用,比後者還多了兩次。
要完成這樣的運行時字元串拼接(註意:是運行時),更佳的做法是使用StringBuilder類型,代碼如下所示:
public static void NewMethod() { ////定義了四個變數 string a = "t"; string b = "e"; string c = "s"; string d = "t"; StringBuilder sb = new StringBuilder(a); sb.Append(b); sb.Append(c); sb.Append(d); ///提示是運行時,所以沒有使用以下代碼 //StringBuilder sb = new StringBuilder("t"); //sb.Append("e"); //sb.Append("s"); //sb.Append("t"); //string result = sb.ToString(); }
微軟還提供了另外一個方法來簡化這種操作,即使用string.Format方法。string.Format方法在內部使用StringBuilder進行字元串的格式化,代碼如下所示:
public static void NewMethod4() { string a = "t"; string b = "e"; string c = "s"; string d = "t"; string result = string.Format("{0}{1}{2}{3}", a, b, c, d); }
對於String和StringBuilder的簡單介紹也可以參考我之前的一篇文章http://www.cnblogs.com/aehyok/p/3505000.html
建議2、使用預設轉型方法
1、使用類型的轉換運算符,其實就是使用類型內部的一方方法(即函數)。轉換運算符分為兩類:隱式轉換和顯式轉換(強制轉換)。基元類型普遍都提供了轉換運算符。
所謂“基元類型”,是指編譯器直接支持的數據類型。基元類型包括:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、bool、decimal、object、string。
int i = 0; float j = 0; j = i; ///int 到float存在一個隱式轉換 i = (int)j; ///float到int必須存在一個顯式轉換
用戶自定義的類型也可以通過重載轉換運算符的方式提供這一類轉換:
public class Ip { IPAddress value; public Ip(string ip) { value = IPAddress.Parse(ip); } //重載轉換運算符,implicit 關鍵字用於聲明隱式的用戶定義類型轉換運算符。 public static implicit operator Ip(string ip) { Ip iptemp = new Ip(ip); return iptemp; } //重寫ToString方法 public override string ToString() { return value.ToString(); } } class Program { public static void Main(string[] args) { Ip ip = "192.168.1.1"; //通過Ip類的重載轉換運算符,實現字元串到Ip類型的隱式轉換 Console.WriteLine(ip.ToString()); Console.ReadLine(); } }
提供的就是字元串到類型Ip之間的隱式轉換。
2、使用類型內置的Parse、TryParse,或者如ToString、ToDouble、ToDateTime等方法
比如從string轉換為int,因為其經常發生,所以int本身就提供了Parse和TryParse方法。一般情況下,如果要對某類型進行轉換操作,建議先查閱該類型的API文檔。
3、使用幫助類提供的方法
可以使用System.Convert類、System.BitConverter類來進行類型的轉換。
System.Convert提供了將一個基元類型轉換為其他基元類型的方法,如ToChar、ToBoolean方法等。值得註意的是,System.Convert還支持將任何自定義類型轉換為任何基元類型,只要自定義類型繼承了IConvertible介面就可以。如上文中的IP類,如果將Ip轉換為string,除了重寫Object的ToString方法外,還可以實現IConvertible的ToString()方法
繼承IConvertible介面必須同時實現其他轉型方法,如上文的ToBoolean、ToByte,如果不支持此類轉型,則應該拋出一個InvalidCastException,而不是一個NotImplementedException。
4、使用CLR支持的轉型
CLR支持的轉型,即上溯轉型和下溯轉型。這個概念首先是在Java中提出來的,實際上就是基類和子類之間的相互轉換。
就比如: 動作Animal類、Dog類繼承Animal類、Cat類也繼承自Amimal類。在進行子類向基類轉型的時候支持隱式轉換,如Dog顯然就是一個Animal;而當Animal轉型為Dog的時候,必須是顯式轉換,因為Animal還可能是一個Cat。
Animal animal = new Animal(); Dog dog = new Dog(); animal = dog; /////隱式轉換,因為Dog就是Animal ///dog=animal; ////編譯不通過 dog = (dog)animal; /////必須存在一個顯式轉換
建議3、區別對待強制轉換與as和is
首先來看一個簡單的實例
FirstType firstType = new FirstType(); SecondType secondType = new SecondType(); secondType = (SecondType)firstType;
從上面的三行代碼可以看出,類似上面的應該就是強制轉換。
首先需要明確強制轉換可能意味這兩件不同的事情:
1、FirstType和SecondType彼此依靠轉換操作來完成兩個類型之間的轉換。
2、FirstType是SecondType的基類。
類型之間如果存在強制轉換,那麼它們之間的關係要麼是第一種,要麼是第二種。不可能同時是繼承的關係,又提供了轉型符。
針對第一種情況:
public class FirstType { public string Name { get; set; } } public class SecondType { public string Name { get; set; } public static explicit operator SecondType(FirstType firstType) { SecondType secondType = new SecondType() { Name = "轉型自:" + firstType.Name }; return secondType; } } class Program { static void Main(string[] args) { FirstType firstType = new FirstType() { Name="First Type"}; SecondType secondType = (SecondType)firstType; ///此轉換是成功的 secondType = firstType as SecondType; ///編譯不通過 Console.ReadLine(); } }
這裡上面也有添加註釋,通過強制轉換是可以轉換成功的,但是使用as運算符是不成功的編譯就不通過。
這裡就是通過轉換符進行處理的結果。
接下來我們再在Program類中添加一個方法
static void DoWithSomeType(object obj) { ///編譯器首先判斷的是,SeondType和ojbect之間有沒有繼承關係。 ///因為在C#中,所有的類型都是繼承自object的,所以這裡編譯沒有什麼問題。 ///但編譯器會自動產生代碼來檢查obj在運行時是不是SecondType,這樣就繞過了操作轉換符,導致轉換失敗。 SecondType secondType = (SecondType)obj; }
如註釋所說的,編譯通過執行報錯的問題。
如果類型之間都上溯到了某個共同的基類,那麼根據此基類進行的轉換(即基類轉型為子類本身),應該使用as。子類與子類之間的轉換,則應該提供轉換操作符,以便進行強制轉換。
現在可以如上方法改寫為
static void DoWithSomeType(object obj) { SecondType secondType = obj as SecondType; }
保證編譯執行都不會報錯。as操作符永遠不會拋出異常,如果類型不匹配(被轉換對象的運行時類型既不是所轉換的目標類型,也不是其派生類型),或者轉型的源對象為null,那麼轉型之後的值也為null。改造前的DoWithSomeType方法會因為引發異常帶來效率問題,而使用as後,就可以完美的避免這種問題。
現在來看第二種情況,即FirstType是SecondType的基類。這種情況下,既可以使用強制轉型又可以使用as操作符。
public class FirstType { public string Name { get; set; } } public class SecondType : FirstType { } class Program { static void Main(string[] args) { SecondType secondType = new SecondType() { Name="aehyok"}; FirstType firstType = (FirstType)secondType; firstType = secondType as FirstType; Console.ReadLine(); } }
但是,即使可以使用強制轉型,從效率的角度來看,也建議大家使用as進行轉型。
下麵再來看一下is操作符。
static void DoWithSomeType(object obj) { if (obj is SecondType) { SecondType secondType = obj as SecondType; } }
這個版本的效率顯然沒有上一個版本的效率高。因為當前這個版本進行了兩次類型檢測。但是,as操作符有個問題,就是它不能操作基元類型。如果涉及到基元類型的演算法,那麼就要使用is進行判斷之後再進行轉型的操作,以避免轉型失敗。