程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則3:選擇is或者as操作符而不是做強制類型轉換

Effective C#原則3:選擇is或者as操作符而不是做強制類型轉換

編輯:關於C#

C#是一個強數據類型語言。好的編程實踐意味著當可以避免從一種數據類型 強制轉化為另種數據類型時,我們應該盡我們的所能來避免它。但在某些時候, 運行時類型檢測是不可避免的。在C#裡,大多數時候你要為調用函數的參數使用 System.Object類型,因為Framwork已經為我們定義了函數的原型。你很可能要 試圖把那些類型進行向下轉化為其它類型的接口或者類。你有兩個選擇:用as運 算符,或者,采用舊式的C風格,強制轉換。(不管是哪一種,)你還必須對變量 進行保護:你可以試著用is進行轉換,然而再用as進行轉換或者強制轉換。

無論何時,正確的選擇是用as運算符進行類型轉換。因為比起盲目的強 制轉換它更安全,而且在運行時效率更高。用as和is運算符進行轉換時,並不是 對所有的用戶定義的類型都能完成的。它們只在運行時類型和目標類型匹配的時 候,轉換才能成功。它們決不會構造一個新的對象來滿足(轉化)要求。

看一個例子。你寫了一段代碼,要轉換一個任意類型的對象實例到一個MyType類 型的實例。你是這樣寫代碼的:

object o = Factory.GetObject( );
// Version one:
MyType t = o as MyType;
if ( t != null )
{
 // work with t, it's a MyType.
} else
{
 // report the failure.
}

或者你這樣寫:

object o = Factory.GetObject( );
// Version two:
try {
 MyType t;
 t = ( MyType ) o;
 if ( t != null )
 {
  // work with T, it's a MyType.
 } else
 {
  // Report a null reference failure.
 }
} catch
{
 // report the conversion failure.
}

你會同意第一種寫法更簡單更容易讀。它沒有try/catch結構, 所以你可以同時避免(性能)開銷和(多寫)代碼。我們注意到,強制轉換的方法為 了檢測轉換是否把一個null的對象進行強制轉換,而不得不添加一個捕獲異常的 結構。null可以被轉換為任意的引用類型,但as操作符就算是轉化一個null的引 用時,也會(安全的)返回一個null。所以當你用強制類型轉換時,就得用一個 try/catch結構來捕獲轉換null時的異常。用as進行轉換的時就,就只用簡單的 檢測一下轉化後的實例不為null就行了。

(譯注:被轉換對象和轉換後的 結果都有可能為null,上面就是對這兩種null進行了說明,注意區分。強制轉換 是不安全的,可能會有異常拋出,因此要用try/catch結構來保證程序正常運行 ,而as轉換是安全的,不會有異常拋出,但在轉換失敗後,其結果為null)

強制轉換與as轉換最大的區別表現在如何對待用戶定義類型的轉換。

與其它運算不一樣,as和is運算符在運行時要檢測轉換目標的類型。如 果一個指定對象不是要求轉換的類型,或者它是從要求轉換類型那裡派生的,轉 換會失敗。另一方面,強制轉換可以用轉換操作把一個對象轉換成要求的類型。 這還包括對內置數據(built-in numberic)類型的轉換。強制轉換一個long到一 個short可能會丟失數據。

同樣的問題也隱藏在對用戶定義類型的轉換上 。考慮這樣的類型:

public class SecondType
{
  private MyType _value;
 // other details elided
 // Conversion operator.
 // This converts a SecondType to
 // a MyType, see item 29.
 public static implicit operator MyType( SecondType t )
 {
  return t._value;
 }
}

假設代碼片段中開始的Factory.GetObject()函數返回的是 SecondType 類型的數據:

object o = Factory.GetObject( );
// o is a SecondType:
MyType t = o as MyType; // Fails. o is not MyType
if ( t != null )
{
 // work with t, it's a MyType.
} else
{
 // report the failure.
}
// Version two:
try {
 MyType t1;
 t = ( MyType ) o; // Fails. o is not MyType
 if ( t1 != null )
  {
  // work with t1, it's a MyType.
 } else
  {
  // Report a null reference failure.
 }
} catch
{
 // report the conversion failure.
}

兩種轉換都失敗了。但是我告訴過你,強制轉化可以在用戶定義 的類型上完成。你應該想到強制轉化會成功。你是對的--(如果)它們跟像你想的 一樣是會成功的。但是轉換失敗了,因為你的編譯器為對象o產生的代碼是基於 編譯時類型。而對於運行時對象o,編譯器什麼也不知道,它們被視為 System.Obejct類型。編譯器認為,不存在System.Object類型到用戶類型MyType 的轉換。它檢測了System.Object和MyType的定義。缺少任意的用戶定義類型轉 換,編譯器(為我們)生成了用於檢測運行時對象o的代碼,並且檢測它是不是 MyType類型。因為對象o是SecondType類型,所以失敗了。編譯器並不去檢測實 際運行時對象o是否可以被轉換為MyType類型。

如果你使用下面的代碼段 ,你應該可以成功的完成從SecondType到MyType的轉換:

object o = Factory.GetObject( );
// Version three:
SecondType st = o as SecondType;
try {
 MyType t;
 t = ( MyType ) st;
 if ( t != null )
 {
  // work with T, it's a MyType.
 } else
 {
  // Report a null reference failure.
 }
} catch
{
 // report the failure.
}

你決不應該寫出如果糟糕的代碼,但它確實解決了一個很常 見的難題。盡管你決不應該這樣寫代碼,但你可以寫一個函數,用一個 System.Object參數來完成正確的轉換:

object o = Factory.GetObject( );
DoStuffWithObject( o );
private void DoStuffWithObject( object o2 )
{
 try {
  MyType t;
  t = ( MyType ) o2; // Fails. o is not MyType
  if ( t != null )
  {
   // work with T, it's a MyType.
  } else
  {
   // Report a null reference failure.
  }
 } catch
 {
  // report the conversion failure.
 }
}

記住,對一 個用戶定義類型的對象,轉換操作只是在編譯時,而不是在運行時。在運行時存 在介於o2和MyType之間的轉換並沒有關系,(因為)編譯器並不知道也不關心這些 。這樣的語句有不同的行為,這要取決於對st類型的申明:

t = ( MyType ) st;

(譯注:上面說的有些模糊。為什麼上面的代碼可能會有不 同的行為的呢?不同的什麼行為呢?主要就是:上面的這個轉化,是在編譯時還 是在運行時!如果st是用戶定義的類型,那麼上面的轉換是在編譯時。編譯器把 st當成為System.Object類型來編譯生成的IL代碼,因此在運行時是無法把一個 Object類型轉化為MyType類型的。解決辦法就是前面提到的方法,多加一條語句 ,先把Object類型轉換為SecondType,然後再強制轉化為MyType類型。但是如果 st是內置類型,那麼它的轉換是在運行時的,這樣的轉化或許會成功,看後面的 說明。因此,類似這樣的代碼:MyType m_mytype = (m_secondType as SecondType) as MyType;是不能通過編譯的,提示錯誤是無法在編譯時把 SecondType轉化為MyType,即使是重寫了轉換操作符。)

但下面的轉換只 會有一種行為,而不管st是什麼類型。

所以你應該選擇as來轉換對象, 而不是強制類型轉換。實際上,如果這些類型與繼承沒有關系,但是用戶自己定 義的轉換操作符是存在的,那麼下面的語句轉換將會得到一個編譯錯誤:t = st as MyType;現在你應該明白要盡可能的使用as,下面我們來討論不能使用as的時 候。as運算符對值類型是無效的,下面的代碼無法通過編譯:

object o = Factory.GetValue( );
int i = o as int; // Does not compile.

這是因為整形(ints)數據是值類型,並且它 們永遠不會為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;

(譯注:is和as一樣,都是類型轉換安全的,它們在任 何時候都不會在轉換時發生異常,因此可以先用is來安全的判斷一下數據類型。 與as不同的時,is只是做類型檢測並返回邏輯值,不做轉換。)

如果o是 其它可轉化為整形的類型(譯注:但o並不是真正的整形),例如double,那麼is 運算操作會返回false。對於null,is總是返回false。

is只應該在你無 法用as進行轉換時使用。

另外,這是無意義的冗余:

// correct, but redundant:
object o = Factory.GetObject( );
MyType t = null;
if ( o is MyType )
 t = o as MyType;

如果你寫下面的代碼,那麼跟上面一樣,都是冗余的:

// correct, but redundant:
object o = Factory.GetObject( );
MyType t = null;
if ( ( o as MyType ) != null )
 t = o as MyType;

這都是低效且冗余的。如 果你使用as來轉換數據,那麼用is來做檢測是不必要的。只用檢測返回類型是否 為null就行了,這很簡單。

現在,你已經知道is,as和強制轉換之間的區 別了。而在foreach的循環中,是使用的哪一種轉換呢?

public void UseCollection( IEnumerable theCollection )
{
 foreach ( MyType t in theCollection )
  t.DoStuff( );
}

foreach循環是用強制轉換來完成把一個對象轉換成循環可用的 類型。上面的循環代碼與下面手寫的代碼(hand-coded)是等效的:

public void UseCollection( IEnumerable theCollection )
{
 IEnumerator it = theCollection.GetEnumerator( );
  while ( it.MoveNext( ) )
 {
  MyType t = ( MyType ) it.Current;
  t.DoStuff( );
 }
}

foreach須要用強制轉換來同時支持對值類型和引用類型的轉換 。通過選擇強制轉化,foreach循環就可以采用一樣的行為,而不用管(循環)目 標對象是什麼類型。不管怎樣,因為foreach循環是用的強制轉換,因些它可能 會產生BadCastExceptions的異常。

因為IEnumberator.Current返回一個 System.Obejct對象,該對象沒有(重寫)轉換操作符,所以它們沒有一個滿足(我 們在上面做的)測試。

(譯注:這裡是說,如果你用一個集合存放了 SecondType,而你又想用MyType來對它進行foreach循環,那麼轉換是失敗的, 原因是在循環時,並不是用SecondType,而是用的System.Object,因此,在 foreach循環裡做的轉換與前面說的:MyType t = ( MyType ) o;是一樣的錯誤 ,這裡的o是SecondType,但是是以System.Object存在。)

正如你已經知 道的,一個存放了SecondType的集合是不能在前面的函數UseCollection中使用 循環的,這會是失敗的。用強制轉換的foreach循環不會在轉換時檢測循環集合 中的對象是否具有有效的運行時類型。它只檢測由IEnumerator.Current返回來 的System.Object是否可轉換為循環中使用的對象類型,這個例子中是MyType類 型。

最後,有些時候你想知道一個對象的精確類型,而不僅僅是滿足當 前可轉換的目標類型。as運算符在為任何一個從目標類型上派生的對象進行轉換 時返回true。GetType()方法取得一個對象的運行時對象,它提供了比is和as更 嚴格的(類型)測試。GetType()返回的類型可以與指定的類型進行比較(,從而知 道它是不是我們想要的類型)。

再次考慮這個函數:

public void UseCollection( IEnumerable theCollection )
{
 foreach ( MyType t in theCollection )
  t.DoStuff( );
}

如果你添加了一個派生自MyType的新類NewType,那麼 一個存放了NewType類型對象的集合可以很好的在UseCollection 函數中工作。

public class NewType : MyType
{
 // contents elided.
}

如果你想要寫一個函數,使它對所有MyType類型 的實例都能工作,上面的方法是非常不錯的。如果你只想寫一個函數,只對精確 的MyType對象有效,那你必須用精確的類型比較。這裡,你可以在foreach循環 的內部完成。大多數時候,認為運行時確定對象的精確類型是很重要的,是在為 對象做相等測試的時候。在大多數其它的比較中,由is和as提供的.isinst(譯注 :IL指令)比較,從語義上講已經是正確的了。

好的面向對象實踐告訴我 們,你應該避免類型轉換,但有些時候我沒別無選擇。當你無法避免轉換時,用 (C#)語言為我們提供的is和as運算符來清楚的表達你的意思吧。不同方法的強制 轉換有不同的規則。從語義上講,is和as在絕大多轉換上正確的,並當目標對象 是正確的類型時,它們總是成功的。應該選擇這些基本指令來轉換對象,至少它 們會如你所期望的那樣成功或者失敗;而不是選擇強制類型轉換,這轉換會產生 一些意想不到的副作用。

小結:翻譯的第三個原則。

今天下午在網上下載了電子版的書籍,因此 翻譯相對快一點點,至少不用自己輸入代碼了,那是很郁悶的事。繼續翻譯,爭 取在春節休假期間把所有的Items翻譯完。

我對書中所說的要注意的事項 基本上都自己測試一下,當然,如果是顯然正確的問題,就直接翻譯了,不做測 試。對於一些有疑點的地方,我盡可能的自己動手測試一下。繼續努力,相信後 面的翻譯會越來越快。

返回教程目錄

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