C#是一門強型別語言。一般情況下,我們最好避免 將一個類型強制轉換為其他類型。但是,有時候運行時類型檢查是無法避免的。相信大家都寫過很多以System.Object類型為參數的函數,因為. NET架構預先為我們定義了這些函數的簽名。在這些函數內部,我們經常要把那些參數向下轉型為其他類型,或者是類,或者是介面。對於這種轉型,我們通常有 兩種選擇:使用as操作符,或者使用傳統C風格的強制轉型。另外還有一種比較保險的做法:先使用is來做一個轉換測試,然後再使用as操作符或者強制轉 型。
正確的選擇應該是儘可能地使用as操作符,因為它比強制轉型要安全,而且在運行時層面也有比較好的效率。需要注意的是,as和is操作符都不執行任何使用者自訂的轉換。只有當運行時類型與目標轉換類型匹配時,它們才會轉換成功。它們永遠不會在轉換過程中構造新的對象。
我們來看一個例子。假如需要將一個任意的對象轉換為一個MyType的執行個體。我們可能會像下面這樣來做:
object o = Factory.GetObject( );
// 第一個版本:
MyType t = o as MyType;
if ( t != null )
{
// 處理t, t現在的類型為MyType。
} else
{
// 報告轉型失敗。
}
或者,也可以像下面這樣來做:
object o = Factory.GetObject( );
// 第二個版本:
try {
MyType t;
t = ( MyType ) o;
if ( t != null )
{
// 處理t, t現在的類型為MyType。
} else
{
// 報告Null 參考失敗。
}
} catch
{
// 報告轉型失敗。
}
相信大家都同意第一個版本的轉型代碼更簡單,也更 容易閱讀。其中沒有添加額外的try/catch語句,因此也就避免了其帶來的負擔。注意,第二個版本中除了要捕捉異常外,還要對null的情況進行檢 查,因為如果o本來就是null,那麼強制轉型可以將它轉換成任何參考型別。但如果是as操作符,且被轉換對象為null,那麼執行結果將返回null。 因此,如果使用強制轉型,我們既要檢查其是否為null,還要捕捉異常。如果使用as操作符,我們只需要檢查返回的引用是否為null就可以了。
cast 和as操作符之間最大的區別就在於如何處理使用者自訂的轉換。操作符as和is都只檢查被轉換對象的運行時類型,並不執行其他的操作。如果被轉換對象的運 行時類型既不是所轉換的目標類型,也不是其衍生類別型,那麼轉型將告失敗。但是,強制轉型則會使用轉換操作符來執行轉型操作,這包括任何內建的數值轉換。例 如,將一個long類型強制轉換為一個short類型將會導致部分資訊丟失。
在我們使用使用者自訂的轉換時,也會有同樣的問題,來看下面的代碼:
public class SecondType
{
private MyType _value;
// 忽略其他細節。
// 轉換操作符。
// 將SecondType 轉換為MyType,參見條款29。[4]
public static implicit operator
MyType( SecondType t )
{
return t._value;
}
}
假設下面第一行代碼中的Factory.GetObject()返回的是一個SecondType對象:
object o = Factory.GetObject( );
// o 為一個SecondType:
MyType t = o as MyType; // 轉型失敗,o的類型不是MyType。
if ( t != null )
{
// 處理t, t現在的類型為MyType。
} else
{
// 報告轉型失敗。
}
// 第二個版本:
try {
MyType t1;
t1 = ( MyType ) o; // 轉型失敗,o的類型不是MyType。
if ( t1 != null )
{
// 處理t1, t1現在的類型為MyType。
} else
{
// 報告Null 參考失敗。
}
} catch
{
// 報告轉型失敗。
}
兩個版本的轉型操作都失敗了。大家應該還記得我前 面說過強制轉型會執行使用者自訂的轉換,有讀者據此認為強制轉型的那個版本會成功。這麼想本身沒有錯誤,只是編譯器在產生代碼時依據的是對象o的編譯時間類 型。編譯器對於o的運行時類型一無所知——編譯器只知道o的類型是System.Object。因此編譯器只會檢查是否存在將System.Object 轉換為MyType的使用者自訂轉換。它會到System.Object類型和MyType類型的定義中去做這樣的檢查。由於沒有找到任何使用者自訂轉 換,編譯器將產生代碼來檢查o的運行時類型,並將其和MyType進行比對。由於o的運行時類型為SecondType,因此轉型將告失敗。編譯器不會檢 查在o的運行時類型SecondType和MyType之間是否存在使用者自訂的轉換。
當然,如果將上述代碼做如下修改,轉換就會成功執行:
object o = Factory.GetObject( );
// 第三個版本:
SecondType st = o as SecondType;
try {
MyType t;
t = ( MyType ) st;
if ( t != null )
{
// 處理t, t現在的類型為MyType。
} else
{
// 報告Null 參考失敗。
}
} catch
{
// 報告轉型失敗。
}
在正式的開發中,我們絕不能寫如此醜陋的代碼,但它卻向我們揭示了問題的所在。雖然大家永遠都不可能像上面那樣寫代碼,但可以使用一個以System.Object類型為參數的函數,讓該函數在內部執行正確的轉換。
object o = Factory.GetObject( );
DoStuffWithObject( o );
private void DoStuffWithObject( object o2 )
{
try {
MyType t;
t = ( MyType ) o2; // 轉型失敗,o的類型不是MyType
if ( t != null )
{
// 處理t, t現在的類型為MyType。
} else
{
// 報告Null 參考失敗。
}
} catch
{
// 報告轉型失敗。
}
}
記住,使用者自訂的轉換操作符只作用於對象的編譯時間類型,而非運行時類型上。至於o2的運行時類型和MyType之間是否存在轉換,並不重要。事實上,編譯器對此並不瞭解,也不關心。對於下面的語句,如果st的宣告類型不同,會有不同的行為:
t = ( MyType ) st;
但對於下面的語句,不管st的宣告類型是什麼,都會產生同樣的結果[5]。因此,我們說as操作符要優於強制轉型——它的轉型結果相對比較一致。
但如果as操作符兩邊的類型沒有繼承關係,即使存在使用者自訂轉換操作符,也會產生編譯時間錯誤。例如,下面的語句:
t = st as MyType;
我們已經知道在轉型的時候應該儘可能地使用as操作符。下面我們來談談一些不能使用as操作符的情況。首先,as操作符不能應用於實值型別。例如,下面的代碼編譯的時候就會報錯:
object o = Factory.GetValue( );
int i = o as int; // 不能通過編譯。
這是因為int是一個實值型別,所以不可以為null。如果o不是一個整數,那這個i裡面還能存放什麼呢?存入的任何值都必須是有效整數,所以as不能和實值型別一起使用。那就只能使用強制轉型了:
object o = Factory.GetValue( );
int i = 0;
try {
i = ( int ) o;
} catch
{
i = 0;
}
但是,我們也並非只能這樣。我們還可以使用is語句來避免其中對異常的檢查或者強制轉型:
object o = Factory.GetValue( );
int i = 0;
if ( o is int )
i = ( int ) o;
如果o是某個其他可以轉換為int的類型,例如double,那麼is操作符將返回false。如果o的值為null,is操作符也將返回false。
只有當我們不能使用as操作符來進行類型轉換時,才應該使用is操作符。否則,使用is將會帶來代碼的冗餘:
// 正確, 但是冗餘:
object o = Factory.GetObject( );
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迴圈語句中使用的是哪個操作符來執行類型轉換呢?
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
答案是強制轉型。事實上,下面的代碼和上面foreach語句編譯後的結果是一樣的:
public void UseCollection( IEnumerable theCollection )
{
IEnumerator it = theCollection.GetEnumerator( );
while ( it.MoveNext( ) )
{
MyType t = ( MyType ) it.Current;
t.DoStuff( );
}
}
之所以使用強制轉型,是因為foreach語句需要同時支援實值型別和參考型別。無論轉換的目標類型是什麼,foreach語句都可以展現相同的行為。但是,由於使用的是強制轉型,foreach語句可能產生BadCastException異常[6]。
由於IEnumerator.Current返回 的是System.Object,而Object中又沒有定義任何的轉換操作符,因此轉換操作符就不必考慮了。如果集合中是一組SecondType對 象,那麼運用在UseCollection()函數中將會出現轉型失敗,因為foreach語句使用的是強制轉型,而強制轉型並不關心集合元素的運行時類 型。它只檢查在System.Object類(由IEnumerator.Current返回的類型)和迴圈變數的宣告類型MyType之間是否存在轉 換。
最後,有時候我們可能想知道一個對象的確切類型, 而並不關心它是否可以轉換為另一種類型。如果一個類型繼承自另一個類型,那麼is操作符將返回true。使用System.Object的GetType ()方法,可以得到一個對象的運行時類型。利用該方法可以對類型進行比is或as更為嚴格的測試,因為我們可以拿它所返回的對象的類型和一個具體的類型做 對比。
再來看下面的函數:
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
如果建立了一個繼承自MyType的類NewType,那便可以將一組NewType對象集合應用在UseCollection函數中。
public class NewType : MyType
{
// 忽略實現細節。
}
如果我們打算編寫一個函數來處理所有與 MyType類型相容的執行個體對象,那麼UseCollection函數所展示的做法就挺好。但如果打算編寫的函數只處理運行時類型為MyType的對象, 那就應該使用GetType()方法來對類型做精確的測試。我們可以將這種測試放在foreach迴圈中。運行時類型測試最常用的地方就是相等判斷(參見 條款9)。對於絕大多數其他的情況,as和is操作符提供的.isinst比較[7]在語義上都是正確的。
條款4:使用Conditional特性代替#if條件編譯 27 |
|
好的物件導向實踐一般都告誡我們要避免轉型,但有時候我們別無選擇。不能避免轉型時,我們應該儘可能地使用C#語言中提供的as和is操作符來更清晰地表 達意圖。不同的轉型方式有不同的規則,is和as操作符絕大多數情況下都能滿足我們的要求,只有當被測試的對象是正確的類型時,它們才會成功。一般情況下 不要使用強制轉型,因為它可能會帶來意想不到的負面效應,而且成功或者失敗往往在我們的預料之外。