# 原則39:使用動態對泛型類型參數的運行時類型的利用
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
System.Linq.Enumerable.Cast<T> 強制序列的每個對象轉化為目標類型 T 。這是框架的一部分,所以 LINQ 查詢中的 IEnumerable (而不是 IEnumerable<T> )才能使用。 Cast<T> 是一個沒有約束的泛型方法。這就是限制類型轉換使用它。如果你理解 Cast<T> 的這個限制,你會發現你自己想的卻不能工作。現實中,它就是該本來那樣工作,而不是你期望的那樣。我們檢查他們的內部工作和限制。然后,你就可以很容易創建不同的版本并像你期望一樣。
問題的根源在于,事實上 Cast<T> 在沒有任何 T 的知識的清下編譯成 MISL ,所以 T 必須是繼承于 System.Object 。因此,它只能使用定義在 System.Object 的函數。查看下面的這個類:
```
public class MyType
{
public String StringMember { get; set; }
public static implicit operator String(MyType aString)
{
return aString.StringMember;
}
public static implicit operator MyType(String aString)
{
return new MyType { StringMember = aString };
}
}
```
查看原則28就知道為什么轉換操作符那么不好;然而,用戶定義的轉換操作符就是這個問題的關鍵。考慮這段代碼(假設 GetSomeStrings() 返回字符串序列):
```
var answer1 = GetSomeStrings().Cast<MyType>();
try
{
foreach (var v in answer1)
Console.WriteLine(v);
}
catch (InvalidCastException)
{
Console.WriteLine("Cast Failed!");
}
```
在開始遍歷元素之前,你可能希望 GetSomeStrings().Cast<MyType>() 已經使用定義在 MyType 的隱式轉換操作符對每個 string 正確轉換為 MyType 。現在你知道它并沒有,它會拋出 InvalidCastException 。
上面的代碼和下面使用查詢表達式是一樣的構造:
```
var answer2 = from MyType v in GetSomeStrings()
select v;
try
{
foreach (var v in answer2)
Console.WriteLine(v);
}
catch (InvalidCastException)
{
Console.WriteLine("Cast failed again");
}
```
在循環體聲明的變量被編譯器調用 Cast<MyType> 。同樣,它會拋出 InvalidCastException 。以下面的方式進行重構,就能工作:
```
var answer3 = from v in GetSomeStrings()
select (MyType)v;
foreach (var v in answer3)
Console.WriteLine(v);
```
有什么不同?前面的兩個版本使用 Cast<T>() 不能工作,這個版本能工作是因為 lambda 的 Select() 的參數使用了強制類型轉換。 Cast<T> 不可能在運行時訪問參數類型任何自定義的轉換操作符。只有引用轉換或封箱轉換才能使用轉換操作符。只有當 is 操作符成功時,引用轉換才會成功(查看原則3)。封箱轉換是將值類型轉換引用類型反之亦然(查看原則45)。 Cast<T> 不能訪問用戶定義的轉換操作符因為它只能假設 T 包含定義在 System.Object 的成員。System.Object 沒有包含任何自定義的轉換操作符,所以這些都是不合格的。使用 Select<T> 成功時因為 lambda 使用 Select() 的輸入參數類型是 string 。這就能使用定義在 MyType 的轉換操作符。
我已經在前面指出過,我常把轉換操作符看著是代碼的小菜。偶爾,它們是有用的,但是靜態是引起比它們的價值更多的問題。如果沒有轉換操作符,這里就不會有開發者寫些不能工作的示例代碼。
當然,如果我建議不要使用轉換操作符,我應該會給一個替代的解決方案。 MyType 已經包含可讀/寫的存儲 string 的屬性,所以我只是把轉換操作符去掉并使用構造函數:
```
var answer4 = GetSomeStrings().
Select(n => new MyType { StringMember = n });
var answer5 = from v in GetSomeStrings()
select new MyType { StringMember = v };
```
這樣,我只需要為 MyType 創建不同的構造函數。當然,這只是針對 Cast<T> 的限制。既然你已經明白為什么這些限制會存在,現在就該寫一個不同方法繞過這個限制。訣竅就是編寫一個泛型方法并利用運行時形象進行類型轉換。
你可能寫了一頁又一頁基于反射的代碼以檢查哪些轉換是可用的,并執行那些轉換返回恰當的類型。你可以那樣做,但是這是浪費。相反, C#4.0 ,動態做了所有繁重的活。你只需要簡單 Convert<T> 就可以得到你期望的:
```
public static IEnumerable<TResult> Convert<TResult>( this System.Collections.IEnumerable sequence)
{
foreach (object item in sequence)
{
dynamic coercion = (dynamic)item;
yield return (TResult)coercion;
}
}
```
現在,只要從源類型到目標類型的轉換(隱式或顯示),轉換就會工作。仍然有強制類型轉換會涉及到,所有運行時失敗還是有可能發生。Convert<T> 與 Cast<T> 的比較中,ConverT<T> 適用于更多的類型轉換的情況,但是它需要做更多工作。作為一個開發者,你應該更多關心用戶需要我們創建什么代碼而不只是我們自己的代碼。 Convert<T> 可以通過整個測試:
```
var convertedSequence = GetSomeStrings().Convert<MyType>();
```
Cast<T> 像其他泛型方法一樣,編譯時對參數類型只有有限掌握。這會導致泛型方法不能像你期望一樣工作。根本原因就是泛型方法不知道類型參數代表的類型的特定的功能。當這種情況發生時,一個小的應用的動態可使運行時反射使問題得到解決。
小結:
這個原則很簡單卻很使用(trick),作者給的建議是:使用泛型參數類型對動態對象進行強制類型轉換。
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2090174](/blog/2090174)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數