7. 無限制的類型參數
假如你創建一個泛型數據結構或類,就象例3中的MyList,注重其中并沒有約束你該使用什么類型來建立參數化類型。然而,這帶來一些限制。如,你不能在參數化類型的實例中使用象==,!=或<等運算符,如:
象==和!=這樣的運算符的實現對于值類型和引用類型都是不同的。假如隨意地答應之,代碼的行為可能很出乎你的意料。另外一種限制是缺省構造器的使用。例如,假如你編碼象new T(),會出現一個編譯錯,因為并非所有的類都有一個無參數的構造器。假如你真正編碼象new T()來創建一個對象,或者使用象==和!=這樣的運算符,情況會是怎樣呢?你可以這樣做,但首先要限制可被用于參數化類型的類型。讀者可以自己先考慮如何實現之。
8. 約束機制及其優點
一個泛型類答應你寫自己的類而不必拘泥于任何類型,但答應你的類的使用者以后可以指定要使用的具體類型。通過對可能會用于參數化的類型的類型施加約束,這給你的編程帶來很大的靈活性--你可以控制建立你自己的類。讓我們分析一個例子:
假定我需要這種類型以支持CompareTo()方法的實現。我能夠通過加以約束--為參數化類型指定的類型必須要實現IComparable接口--來指定這一點。例6中的代碼就是這樣:
你可以指定約束的組合,就象: where T : IComparable, new()。這就是說,用于參數化類型的類型必須實現Icomparable接口并且必須有一個無參構造器。
9. 繼續與泛型
10. 泛型和可代替性
當我們使用泛型時,要小心可代替性的情況。假如B繼續自A,那么在使用對象A的地方,可能都會用到對象B。假定我們有一籃子水果(a Basket of Fruits (Basket<Fruit>)),而且有繼續自Fruit的Apple和Banana(皆為Fruit的種類)。一籃子蘋果--Basket of Apples (Basket<apple>)可以繼續自Basket of Fruits (Basket<Fruit>)?答案是否定的,假如我們考慮一下可代替性的話。為什么?請考慮一個a Basket of Fruits可以工作的方法:
假如發送一個Basket<Fruit>的實例給這個方法,這個方法將添加一個Apple對象和一個Banana對象。然而,發送一個Basket<Apple>的實例給這個方法時,會是什么情形呢?你看,這里布滿技巧。這解釋了為什么下列代碼:
這在上面的例中在成功的,但也存在非凡情形:有時我們確實想傳遞一個集合的派生類,此時需要一個集合的基類。例如,考慮一下Animal(如Monkey),它有一個把Basket<Fruit>作參數的方法Eat,如下所示:
假如你有一籃子(a Basket of)Banana-一Basket<Banana>,情況會是如何呢?把一籃子(a Basket of)Banana-一Basket<Banana>發送給Eat方法有意義嗎?在這種情形下,會成功嗎?真是這樣的話,編譯器會給出錯誤信息:
11. 泛型和代理
代理也可以是泛型化的。這樣就帶來了巨大的靈活性。
假定我們對寫一個框架程序很感愛好。我們需要提供一種機制給事件源以使之可以與對該事件感愛好的對象進行通訊。我們的框架可能無法控制事件是什么。你可能在處理某種股票價格變化(double price),而我可能在處理水壺中的溫度變化(temperature value),這里Temperature可以是一種具有值、單位、門檻值等信息的對象。那么,怎樣為這些事件定義一接口呢?
讓我們通過pre-generic代理技術細致地分析一下如何實現這些:
public delegate void NotifyDelegate(Object info);
public interface ISource
{
event NotifyDelegate NotifyActivity;
}
我們讓NotifyDelegate接受一個對象。這是我們過去采取的最好措施,因為Object可以用來代表不同類型,如double,Temperature,等等--盡管Object含有因值類型而產生的裝箱的開銷。ISource是一個各種不同的源都會支持的接口。這里的框架展露了NotifyDelegate代理和ISource接口。
讓我們看兩個不同的
源碼:
public class StockPriceSource : ISource
{
public event NotifyDelegate NotifyActivity;
//…
}
public class BoilerSource : ISource
{
public event NotifyDelegate NotifyActivity;
//…
}
假如我們各有一個上面每個類的對象,我們將為事件注冊一個處理器,如下所示:
StockPriceSource stockSource = new StockPriceSource();
stockSource.NotifyActivity
+= new NotifyDelegate(stockSource_NotifyActivity);
//這里不必要出現在同一個程序中
BoilerSource boilerSource = new BoilerSource();
boilerSource.NotifyActivity
+= new NotifyDelegate(boilerSource_NotifyActivity);
在代理處理器方法中,我們要做下面一些事情:
對于股票事件處理器,我們有:
void stockSource_NotifyActivity(object info)
{
double price = (double)info;
//在使用前downcast需要的類型
}
溫度事件的處理器看上去會是:
void boilerSource_NotifyActivity(object info)
{
Temperature value = info as Temperature;
//在使用前downcast需要的類型
}
上面的代碼并不直觀,且因使用downcast而有些凌亂。借助于泛型,代碼將變得更易讀且更輕易使用。讓我們看一下泛型的工作原理:
下面是代理和接口:
public delegate void NotifyDelegate<t>(T info);
public interface ISource<t>
{
event NotifyDelegate<t> NotifyActivity;
}
我們已經參數化了代理和接口?,F在的接口的實現中應該能確定這是一種什么類型。
Stock的源代碼看上去象這樣:
public class StockPriceSource : ISource<double>
{
public event NotifyDelegate<double> NotifyActivity;
//…
}
而Boiler的源代碼看上去象這樣:
public class BoilerSource : ISource<temperature>
{
public event NotifyDelegate<temperature> NotifyActivity;
//…
}
假如我們各有一個上面每種類的對象,我們將象下面這樣來為事件注冊一處理器:
StockPriceSource stockSource = new StockPriceSource();
stockSource.NotifyActivity += new NotifyDelegate<double>(stockSource_NotifyActivity);
//這里不必要出現在同一個程序中
BoilerSource boilerSource = new BoilerSource();
boilerSource.NotifyActivity += new NotifyDelegate<temperature>(boilerSource_NotifyActivity);
現在,股票價格的事件處理器會是:
void stockSource_NotifyActivity(double info)
{ //… }
溫度的事件處理器是:
void boilerSource_NotifyActivity(Temperature info)
{ //… }
這里的代碼沒有作downcast并且使用的類型是很清楚的。
12. 泛型與反射
既然泛型是在CLR級上得到支持的,你可以使用反射API來取得關于泛型的信息。假如你是編程的新手,可能有一件事讓你迷惑:你必須記住既有你寫的泛型類也有在運行時從該泛型類創建的類型。因此,當使用反射API時,你需要另外記住你在使用哪一種類型。我將在例7說明這一點:
例7.在泛型上的反射
public class MyClass<t> { }
class Program
{
static void Main(string[] args)
{
MyClass<int> obj1 = new MyClass<int>();
MyClass<double> obj2 = new MyClass<double>();
Type type1 = obj1.GetType();
Type type2 = obj2.GetType();
Console.WriteLine("obj1’s Type");
Console.WriteLine(type1.FullName);
Console.WriteLine(type1.GetGenericTypeDefinition().FullName);
Console.WriteLine("obj2’s Type");
Console.WriteLine(type2.FullName);
Console.WriteLine(type2.GetGenericTypeDefinition().FullName);
}
}
在本例中,有一個MyClass<int>的實例,程序中要查詢該實例的類名。然后我查詢這種類型的GenericTypeDefinition()。GenericTypeDefinition()會返回MyClass<T>的類型元數據。你可以調用IsGenericTypeDefinition來查詢是否這是一個泛型類型(象MyClass<T>)或者是否已指定它的類型參數(象MyClass<int>)。同樣地,我查詢MyClass<double>的實例的元數據。上面的程序輸出如下:
obj1’s Type
TestApp.MyClass`1
[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]]
TestApp.MyClass`1
obj2’s Type
TestApp.MyClass`1
[[System.Double, mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089]]
TestApp.MyClass`1
可以看到,MyClass<int>和MyClass<double>是屬于mscorlib配件集的類(動態創建的),而類MyClass<t>屬于我自建的配件集。
13. 泛型的局限性
至此,我們已了解了泛型的強大威力。是否其也有不足呢?我發現了一處。我希望微軟能夠明確指出泛型存在的這一局制性。在表達約束的時候,我們能指定參數類型必須繼續自一個類。然而,指定參數必須是某種類的基類型該如何呢?為什么要那樣做呢?
在例4中,我展示了一個Copy()方法,它能夠把一個源List的內容復制到一個目標list中去。我可以象如下方式使用它:
List<Apple> appleList1 = new List<Apple>();
List<Apple> appleList2 = new List<Apple>();
…
Copy(appleList1, appleList2);
然而,假如我想要把apple對象從一個列表復制到另一個Fruit列表(Apple繼續自Fruit),情況會如何呢?當然,一個Fruit列表可以容納Apple對象。所以我要這樣編寫代碼:
List<Apple> appleList1 = new List<Apple>();
List<Fruit> fruitsList2 = new List<Fruit>();
…
Copy(appleList1, fruitsList2);
這不會成功編譯。你將得到一個錯誤:
Error 1 The type arguments for method
’TestApp.Program.Copy<t>(System.Collections.Generic.List<t>,
System.Collections.Generic.List<t>)’ cannot be inferred from the usage.
編譯器基于調用參數并不能決定T應該是什么。其實我想說,Copy方法應該接受一個某種數據類型的List作為第一個參數,一個相同類型的List或者它的基類型的List作為第二個參數。
盡管無法說明一種類型必須是另外一種類型的基類型,但是你可以通過仍然使用約束機制來克服這一限制。下面是這種方法的實現:
public static void Copy<T, E>(List<t> source,
List<e> destination) where T : E
在此,我已指定類型T必須和E屬同一種類型或者是E的子類型。我們很幸運。為什么?T和E在這里都定義了!我們能夠指定這種約束(然而,C#中并不鼓勵當E也被定義的時候使用E來定義對T的約束)。
然而,請考慮下列的代碼:
public class MyList<t>
{
public void CopyTo(MyList<t> destination)
{
//…
}
}
我應該能夠調用CopyTo:
MyList<apple> appleList = new MyList<apple>();
MyList<apple> appleList2 = new MyList<apple>();
//…
appleList.CopyTo(appleList2);
我也必須這樣做:
MyList<apple> appleList = new MyList<apple>();
MyList<fruit> fruitList2 = new MyList<fruit>();
//…
appleList.CopyTo(fruitList2);
這當然不會成功。如何修改呢?我們說,CopyTo()的參數可以是某種類型的MyList或者是這種類型的基類型的MyList。然而,約束機制不答應我們指定一個基類型。下面情況又該如何呢?
public void CopyTo<e>(MyList<e> destination) where T : E
抱歉,這并不工作。它將給出一個編譯錯誤:
Error 1 ’TestApp.MyList<t>.CopyTo<e>()’ does not define type
parameter ’T’
當然,你可以把代碼寫成接收任意類型的MyList,然后在代碼中,校驗該類型是可以接收的類型。然而,這把檢查工作推到了運行時刻,丟掉了編譯時類型安全的優點。
14. 結論
.NET 2.0中的泛型是強有力的,你寫的代碼不必限定于一特定類型,然而你的代碼卻能具有類型安全性。泛型的實現目標是既提高程序的性能又不造成代碼的臃腫。然而,在它的約束機制存在不足(無法指定一類型必須是另外一種類型的基類型)的同時,該約束機制也給你書寫代碼帶來很大的靈活性,因為你不必拘泥于各種類型的"最小公分母"能力。進入討論組討論。