原則三:使用is 和 as 而不是用強制類型轉換
prefer the is or as operators to casts
投入到C#的懷抱,你就投入到了強類型(strong type)的懷抱(譯注:C#是強類型語言)。這在大部分情況下是有好處的。強類型意味着你希望編譯器能找出代碼中類型不比對的地方。這也意味着你的應用程式在運作時不用做太多的類型檢查。但有些時候,運作時的類型檢查是不可避免的。有時你需要寫一些使用object作為參數的方法(假設因為架構中定義了這些方法的簽名,使你不得不這麼做),你很可能需要将這些object轉換成其它類型,不管是類(class)還是接口(interface)。你有兩個選擇:要麼使用as操作符,要麼使用cast強制轉換。你也可以使用一個穩妥的變通方法:先用is對這個類型轉換進行測試,再用as或者cast進行轉換。
正确的選擇是:在所有能使用as操作符的地方盡可能地使用它。因為與盲目的強制轉換比起來,它更安全而且更高效。As 和is操作符都不進行任何使用者定義的轉換。它們僅在運作時的類型與目标類型相比對的情況下才傳回成功。它們不會為了滿足功能而去創造一個新的對象。
來看一個例子,你想将任意類型的對象(object)轉化成MyType的執行個體(instance),你可以寫成這樣:
//Version one
object o = Factory.GetObject();
MyType t = o as MyType;
if(t != null)
{
//用t來處理事務
}
else
{
//報告錯誤
}
也可以寫成下面這個樣子:
//Version two
object o = Factory.GetObject();
try
{
MyType t ;
t=(MyType)o;
//用t來處理事務
}
catch(InvalidCastException)
{
//報告錯誤
}
你不得不承認第一個版本更簡單、更易讀。它沒有使用try/catch,是以你即節省了運作時開銷,也精簡了代碼。注意,在使用強制轉換的版本中,為了捕獲異常,你必須額外地去檢驗類型是否為null。使用強制轉換可以将null轉化成任何引用類型,但是對于as操作,當指向一個空引用的時候,會傳回一個null值。是以,使用強制轉換,你必須檢驗是否為null并捕獲異常(譯注:但是舉得例子裡并沒有展現啊!)。而使用as,你隻需将傳回的引用與null值進行比較就可以了。
(譯注:說了這麼一大段,中心思想就是:用as你隻要檢驗是否為null就可以了,而用強制轉換你不但要檢驗是否為null,還有捕獲、處理異常)。
As和強制轉換最大的差別在于他們對使用者定義轉換的處理。As和is操作符隻是檢驗要轉換的運作時類型,并不做其它操作。如果所檢驗的類型不是目标類型或從目标類型派生出來的類型,它們檢驗失敗。強制轉換則相反,它使用轉換操作符将一個對象轉換成需要的類型。這包括所有的内建數值類型的轉換。将long類型轉換成short類型,可能會丢失部分資料。
當你對使用者自定義類型進行強制轉換的時候,也可能出現問題。看一下下面的類:
public class SecondType
{
private MyType _value;
//轉換操作符。将一個SecondType轉化為MyType。詳見原則9
public static implicit operator MyType(SecondType t)
{
return t._value;
}
}
假設在以下這段代碼中,有一個SecondType的對象是由Factory.GetObject()函數傳回的:
//version one:
object o = Factory.GetObject();
//o 是 SecondType:
MyType t= o as MyType;//轉換失敗,o不是MyType
if(t!=null)
{
}
else
{
}
//version two:
object o = Factory.GetObject();
try
{
MyType t1;
t1 = (MyType)o;//轉換失敗,o不是MyType
}
catch(InvalidCastException)
{
}
兩個版本都轉換失敗。但是,強制類型轉換是是會執行了使用者自定義的轉換。你可能會想這樣的話(既然執行了使用者自定義的轉換)那強制轉換應該是能成功轉換的。沒錯——如果按照這種思路來說,它是應該成功的。但實際上它還是失敗了,因為編譯器産生的代碼是基于編譯時對象o的類型的。編譯器并不知道在運作時對象o的實際類型,它将o當成一個object的執行個體。編譯器認為沒有從object類型轉到MyType類型的使用者自定義轉換。它檢查了object和MyType的定義,沒有任何的使用者自定義轉換,編譯器生成代碼來檢查o的類型,并檢查它是否是MyType類型。因為o是SecondType類型,是以這樣的轉換失敗。編譯器并不會去檢驗運作時o的實際類型是否可以轉換成MyType類型。
如果按照以下代碼的寫法,你可以成功地将SecondType類型轉換成MyType類型:
//version three:
object o = Factory.GetObject();
SecondType st = o as SecondType;
try
{
MyType t;
t= (MyType)st;
}
catch(InvalidCastException)
{
}
你永遠都不應該寫出如此醜陋的代碼,但它确實解決了一個常見的問題。你可以将一個object對象作為函數的參數并希望它能夠進行适當的類型轉換,盡管如此,你還是盡量不要這麼做:
object o = Factory.GetObject();
DoStuffWithObject(o);
private static void DoStuffWithObject(Object o)
{
try
{
MyType t;
t = (MyType)o;//失敗,o不是MyType類型。
}
catch
{
}
}
記住,使用者自定義轉換操作隻對于編譯時類型有效,而對于運作時類型無效。運作時有一個從o類型向MyType類型轉換也沒用,編譯器不知道或者不在乎。下面的這個語句,當st聲明不同的類型的時候它會産生不同的行為:
t= (MyType)st;
而下面這個語句,不管st聲明型是什麼,它都傳回相同的結果。是以,比起強制轉換你應該更喜歡as——它一緻性更好。事實上,如果它們的類型關系不是繼承,并且存在它們間的使用者自定義轉換操作,下面這個語句将會産生一個編譯錯誤:
t= st as MyType;
現在,你知道了在能使用as的地方應該盡可能地使用as,我們再來讨論一下什麼時候不能使用as。As操作符不能對值類型進行操作,下面這段代碼是無法通過編譯的:
object o = Factory.GetObject();
int i = o as int;
這是因為int是值類型,值類型永遠都不能為null。是以,如果當o不是一個整形數的時候,i應該存儲什麼值呢?不管你選什麼數,它都是一個有效的整形。是以,as在這種情況下就不能使用。你又掙紮于使用強制轉換,實際上,這将是一個裝箱/拆箱的轉換(詳見原則45):
object o = Factory.GetValue();
int i = 0;
try
{
i = (int)o;
}
catch(InvalidCastException)
{
i=0;
}
使用異常機制是一種不好的習慣(譯注:當然,在你沒得選的時候,用異常機制總比不用的好)。你可以使用is來消除可能因類型轉換而引發的異常,這樣你就可以不使用異常機制了:
object o = Factory.GetValue();
int i = 0;
if(o is int)
i= (int)o;
如果o是那些可以轉化成int的資料類型的時候,例如double,is操作将傳回true,否則傳回false。對于null參數,它也是傳回false。
隻有當你不能使用as來做類型轉換的時候,才能使用is。否則,它将産生備援:
//正确,但是備援
MyType t = null;
if(o is MyType)
t= o as MyType;
上面這段代碼的效果和下面這段代碼是一樣的:
//正确,但是備援
object o = Factory.GetObject();
MyType t = null;
if((o as MyType) != null)
t= o as MyType;
這都是備援而且低效的,如果你打算用as來做類型轉換,那麼用is進行檢查就沒有必要了。檢查as傳回的的值是否為null就更沒必要了。
現在,你已經知道is、as和強制轉換的差別了。但是,在foreach循環中,你應該使用哪個呢?foreach循環可以對非泛型的IEnumerable序列進行操作,它擁有内建的強制轉換。(如果可能的話,應該盡量使用安全類型的泛型版本,但這些非泛型版本的存在是有曆史原因的,他們是用來支援一些晚綁定的情況)
public void UseCollection(IEnumerable theCollection)
{
foreach(MyType t in theCollection)
t.DoStuff();
}
foreach通過強制轉換将對象轉換成目标類型。上面這段由foreach産生的代碼大緻與以下這段代碼相同:
public void UseCollection(IEnumerable theCollection)
{
Enumerator it = theCollection.GetEnumerator();
while(it.MoveNext)
{
MyType t = (MyType)it.Current;
t.DoStuff();
}
}
為了同時支援值類型和引用類型,foreach必須使用強制轉換。通過使用強制轉換,不管目标類型是什麼,foreach都可以表現出同樣的行為。盡管如此,因為使用的是強制轉換,foreach循環可能會抛出一個InvalidCastException(無效的類型轉換)錯誤。
IEnumerator.Current傳回的是一個System.Object類型的對象,而System.Object又沒有強制轉換操作符,是以以下這些嘗試都是不能成功的。一個SecondType的對象的集合是不能使用前面那個UseCollection函數的,因為它會導緻類型轉換失敗,這個前面我們已經讨論過了。Foreach語句(它使用的是強制轉換)并不會在運作時檢驗集合中的類型可否轉換成目标類型(它是在編譯時做的)。它隻是檢查是否可以由(從IEnumerator.Current傳回的)System.Object類型轉換成循環中定義的變量類型(本例中是MyType)。
有時候,你想要知道一個對象的确切類型,而不僅僅是它能否從目前類型轉換成目标類型。如果目前類型是派生自目标類型的話,is操作符将傳回true。GetType()方法能夠擷取一個對象的運作時的類型。這樣它就比is和as更加精确。GetType()傳回一個對象的類型,并且可以和另一個具體的類型進行比較。
再次考慮一下這個函數:
public void UseCollectionV3(IEnumerable theCollection)
{
foreach(MyType t in theCollection)
t.DoStuff();
}
如果你定義一個派生自MyType的類型NewType,NewType的集合就可以正常地使用UseCollection()函數了:
public class NewType:MyType
{
}
如果你想要寫一個函數,它能夠對所有MyType執行個體都有效(包括派生自MyType的),上面的寫法沒問題。但是,如果你想寫一個函數,它隻對類型确切為MyType的對象(而不是MyType的子類對象)有效,你應該使用精确的類型比較。在這,你可以在循環的内部去實作。大多數時候,當做相等性檢查的時候,運作時的确切的類型是非常重要的(詳見原則六)。 而在其它比較的時候,由is和as提供的.isinst(譯注:IL指令)比較,從文法上已經正确的了。
.NET的基礎類庫(BCL:Base Class Library)包含了一個方法,這個方法能夠使隊列中的元素使用相同的類型操作:Enumerable.Cast<T>()能夠轉換所有支援典型IEnumerable接口的隊列的所有元素。
IEnumerable collection = new List<int>(){1,2,3,4,5,6,7,8,9,10};
var small = from int item in collection
while item <5
select itme;
var small2 = collection.Cast<int>().Where(item => item <5 ).select(n => n)
(譯注:以上這段代碼是什麼?我還真看不懂,有哪位同學看懂了的,麻煩解釋一下。)
上面的查詢産生了與最後一行一樣的代碼調用。在上面的兩種情況中,Cast<T>方法将每一個item都轉化成目标類型隊列中的一個成員。Enumerable.Cast<T>是用舊的強制轉換,而不是as操作符。使用舊的強制轉換就意味着Cast<T>不必具有一個類型限制。如果使用as操作符就受到限制,相比較于實作多種不同的Cast<T>方法,BCL團隊甯願選擇建立一個單一的隻使用強制轉換的方法。你在自己的代碼中也要做這樣的權衡。在一些場合,你需要轉換一些泛型參數對象的類型,你就需要權衡一下對類型的必要的一些限制與采用不同處理方法進行轉換的利弊了。(有限制會更安全,但同時就需要提供不同的轉換方法。你需要在安全和友善之間做一個選擇)
在C#4.0,通過使用動态和運作時類型檢查,能夠規避類型系統。這是第五章——Dynamic programming in C#——中所要讨論的問題。(譯注:原書的結構是每幾個原則會組成一個章,比如原則1-11是第一章:C# Language Idioms。我會在所有原則翻譯完後,重新整理一下書的結構,發一個完整版的)。很少有途徑去處理一個對象是基于希望了解它的行為而不是了解它詳細的類型或是接口支援。你需要知道什麼時候要使用這個技術而什麼時候要避免。
好的面向對象程式設計思想告訴我們應該盡量避免類型轉換,但是,總是會有一些異類存在。當你不可避免的要使用類型轉換的時候,應該用as和is來更清晰地表達你的意圖。不同的類型轉換方法有着不同的規則,is和as操作符幾乎總是擁有正确的語義,他們隻有在類型被證明是正确的時候才會轉換成功。更喜歡那些強制轉換語句,它會擁有一些意外的影響,不管成功或是失敗,你都不會對它抱有太大希望。(即不确定性更強)都tem詢産生了與最後一行一樣的代碼調用,