NET 2.0中泛型

iDotNetSpace發表於2009-01-13
前言

  .NET 2.0中泛型的出現是一個令人激動的特徵。但是,什麼是泛型?你需要它們嗎?你會在自己的應用軟體中使用它們?在本文中,我們將回答這些問題並細緻地分析泛型的使用,能力及其侷限性。

  型別安全

  .NET中的許多語言如C#,C++和VB.NET(選項strict為on)都是強型別語言。作為一個程式設計師,當你使用這些語言時,總會期望 編譯器進行型別安全的檢查。例如,如果你把對一個Book型別的引用轉換成一個Vehicle型的引用,編譯器將告訴你這樣的cast是無效的。

  然而,當談到.NET 1.0和1.1中的集合時,它們是無助於型別安全的。請考慮一個ArrayList的例子,它擁有一個物件集合--這允許你把任何型別的物件放於該ArrayList中。讓我們看一下例1中的程式碼。

  例1.缺乏型別安全的ArrayList

using System;
using System.Collections;
namespace TestApp
{
class Test
{
[STAThread]
static void Main(string[] args)
{
ArrayList list = new ArrayList();
list.Add(3);
list.Add(4);
//list.Add(5.0);
int total = 0;
foreach(int val in list)
{
total = total + val;
}
Console.WriteLine("Total is {0}", total);
}
}
}

  本例中,我們建立了一個ArrayList的例項,並把3和4新增給它。然後我迴圈遍歷該ArrayList,從中取出整型值然後把它們相加。這個程式將產生結果"Total is 7"。現在,如果我註釋掉下面這句:

list.Add(5.0);

  程式將產生如下的執行時刻異常:

Unhandled Exception: System.InvalidCastException: Specified cast is not valid.
AtTestApp.Test.Main(String[]args)in :"workarea"testapp"class1.cs:line 17

  哪裡出錯了呢?記住ArrayList擁有一個集合的物件。當你把3加到ArrayList上時,你已把值3裝箱了。當你迴圈該列表時,你是把 元素拆箱成int型。然而,當你新增值5.0時,你在裝箱一個double型值。在第17行,那個double值被拆箱成一個int型。這就是失敗的原 因。

  注意:上面的例項,如果是用VB.NET書寫的話,是不會失敗的。原因在於,VB.NET不使用裝箱機制,它啟用一個把該double轉換成整型的方法。但是,如果ArrayList中的值是不能轉換成整型的,VB.NET程式碼還會失敗。

  作為一個習慣於使用語言提供的型別安全的程式設計師,你希望這樣的問題在編譯期間浮出水面,而不是在執行時刻。這正是泛型產生的原因。

  3. 什麼是泛型?

  泛型允許你在編譯時間實現型別安全。它們允許你建立一個資料結構而不限於一特定的資料型別。然而,當使用該資料結構時,編譯器保證它使用的型別 與型別安全是相一致的。泛型提供了型別安全,但是沒有造成任何效能損失和程式碼臃腫。在這方面,它們很類似於C++中的模板,不過它們在實現上是很不同的。

  4. 使用泛型集合

  .NET 2.0的System.Collections.Generics 名稱空間包含了泛型集合定義。各種不同的集合/容器類都被"引數化"了。為使用它們,只需簡單地指定引數化的型別即可。請看例2:

  例2.型別安全的泛型列表

List<int> aList = new List<int>();
aList.Add(3);
aList.Add(4);
// aList.Add(5.0);
int total = 0;
foreach(int val in aList)
{
total = total + val;
}
Console.WriteLine("Total is {0}", total);

  在例2中,我編寫了一個泛型的列表的例子,在尖括號內指定引數型別為int。該程式碼的執行將產生結果"Total is 7"。現在,如果我去掉語句doubleList.Add(5.0)的註釋,我將得到一個編譯錯誤。編譯器指出它不能傳送值5.0到方法Add(),因為 該方法僅接受int型。不同於例1,這裡的程式碼實現了型別安全。

  5. CLR對於泛型的支援

  泛型不僅是一個語言級上的特徵。.NET CLR能識別出泛型。在這種意義上說,泛型的使用是.NET中最為優秀的特徵之一。對每個用於泛型化的型別的引數,類也同樣沒有脫離開微軟中間語言 (MSIL)。換句話說,你的配件集僅包含你的引數化的資料結構或類的一個定義,而不管使用多少種不同的型別來表達該引數化的型別。例如,如果你定義一個 泛型型別MyList<T>,僅僅該型別的一個定義出現在MSIL中。當程式執行時,不同的類被動態地建立,每個類對應該引數化型別的一種型別。如果你使 用MyList<int>和MyList<double>,有兩種類即被建立。當你的程式執行時,讓我們進一步在例3中分析這一點。

  例3.建立一個泛型類

//MyList.cs
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
#endregion
namespace CLRSupportExample
{
public class MyList<T>
{
private static int bjCount = 0;
public MyList()
{objCount++; }
public int Count
{
get
{return objCount; }
}
}
}
//Program.cs
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
#endregion
namespace CLRSupportExample
{
class SampleClass {}
class Program
{
static void Main(string[] args)
{
MyList<int> myIntList = new MyList<int>();
MyList<int> myIntList2 = new MyList<int>();
MyList<double> myDoubleList = new MyList<double>();
MyList<SampleClass> mySampleList = new MyList<SampleClass>();
Console.WriteLine(myIntList.Count);
Console.WriteLine(myIntList2.Count);
Console.WriteLine(myDoubleList.Count);
Console.WriteLine(mySampleList.Count);
Console.WriteLine(new MyList<sampleclass>().Count);
Console.ReadLine();
}
}
}

  該例中,我建立了一個稱為MyList泛型類。為把它引數化,我簡單地插入了一個尖括號。在<>內的T代表了實際的當使用該類時要指定的型別。 在MyList類中,定義了一個靜態欄位objCount。我在構造器中增加它的值。因此我能發現使用我的類的使用者共建立了多少個那種型別的物件。屬性 Count返回與被呼叫的例項同型別的例項的數目。

  在Main()方法,我建立了MyList<int>的兩個例項,一個MyList<double>的例項,還有兩個MyList <SampleClass>的例項--其中SampleClass是我已定義了的類。問題是:Count(上面的程式的輸出)的值該是多少?在你繼閱讀之 前,試一試回答這個問題。

  解決了上面的問題?你得到下列的答案了嗎?

2
2
1
1
2

  前面兩個2對應MyList<int>,第一個1對應MyList<double>,第二個1對應MyList<SampleClass>-- 在此,僅建立一個這種型別的例項。最後一個2對應MyList<SampleClass>,因為程式碼中又建立了這種型別的另外一個例項。上面的例子說明 MyList<int>是一個與MyList<double>不同的類,而MyList<double>又是一個與MyList <SampleClass>不同的類。因此,在這個例中,我們有四個類:MyList: MyList<T>,MyList<int>,MyList<double>和MyList<X>。注意,雖然有4個MyList類,但僅有一個被儲存在 MSIL。怎麼能證明這一點?請看圖1顯示出的使用工具ildasm.exe生成的MSIL程式碼。

針對任何型別物件:深入淺出.NET泛型程式設計
圖 1.例3的MSIL

  6. 泛型方法

  除了有泛型類,你也可以有泛型方法。泛型方法可以是任何類的一部分。讓我們看一下例4:

  例4.一個泛型方法

public class Program
{
public static void Copy<T>(List<T> source, List<T> destination)
{
foreach (T obj in source)
{
destination.Add(obj);
}
}
static void Main(string[] args)
{
List<int> lst1 = new List<int>();
lst1.Add(2);
lst1.Add(4);
List<int> lst2 = new List<int>();
Copy(lst1, lst2);
Console.WriteLine(lst2.Count);
}
}

  Copy()方法就是一個泛型方法,它與引數化的型別T一起工作。當在Main()中啟用Copy()時,編譯器根據提供給Copy()方法的引數確定出要使用的具體型別。

7. 無限制的型別引數

  如果你建立一個泛型資料結構或類,就象例3中的MyList,注意其中並沒有約束你該使用什麼型別來建立引數化型別。然而,這帶來一些限制。如,你不能在引數化型別的例項中使用象==,!=或<等運算子,如:

if (obj1 == obj2) …

象==和!=這樣的運算子的實現對於值型別和引用型別都是不同的。如果隨意地允許之,程式碼的行為可能很出乎你的意料。另外一種限制是預設構造 器的使用。例如,如果你編碼象new T(),會出現一個編譯錯,因為並非所有的類都有一個無引數的構造器。如果你真正編碼象new T()來建立一個物件,或者使用象==和!=這樣的運算子,情況會是怎樣呢?你可以這樣做,但首先要限制可被用於引數化型別的型別。讀者可以自己先考慮如 何實現之。

8. 約束機制及其優點

一個泛型類允許你寫自己的類而不必拘泥於任何型別,但允許你的類的使用者以後可以指定要使用的具體型別。通過對可能會用於引數化的型別的型別施加約束,這給你的程式設計帶來很大的靈活性--你可以控制建立你自己的類。讓我們分析一個例子:

例5.需要約束:程式碼不會編譯成功

public static T Max<T>(T op1, T op2)
{
if (op1.CompareTo(op2) < 0)
return op1;
return op2;
}

例5中的程式碼將產生一個編譯錯誤:

Error 1 ’T’ does not contain a definition for ’CompareTo’

假定我需要這種型別以支援CompareTo()方法的實現。我能夠通過加以約束--為引數化型別指定的型別必須要實現IComparable介面--來指定這一點。例6中的程式碼就是這樣:

例6.指定一個約束

public static T Max<T>(T op1, T op2) where T : IComparable
{
if (op1.CompareTo(op2) < 0)
return op1;
return op2;
}

在例6中,我指定的約束是,用於引數化型別的型別必須繼承自(實現)Icomparable。下面的約束是可以使用的:

where T : struct 型別必須是一種值型別(struct)

where T : class 型別必須是一種引用型別(class)

where T : new() 型別必須有一個無引數的構造器

where T : class_name 型別可以是class_name或者是它的一個子類

where T : interface_name 型別必須實現指定的介面

你可以指定約束的組合,就象: where T : IComparable, new()。這就是說,用於引數化型別的型別必須實現Icomparable介面並且必須有一個無參構造器。

9. 繼承與泛型

一個使用引數化型別的泛型類,象MyClass1<T>,稱作開放結構的泛型。一個不使用引數化型別的泛型類,象MyClass1<int>,稱作封閉結構的泛型。

你可以從一個封閉結構的泛型進行派生;也就是說,你可以從另外一個稱為MyClass1的類派生一個稱為MyClass2的類,就象:

public class MyClass2<T> : MyClass1<int>

你也可以從一個開放結構的泛型進行派生,如果型別被引數化的話,如:

public class MyClass2<T> : MyClass2<T>

是有效的,但是

public class MyClass2<T> : MyClass2<Y>

是無效的,這裡Y是一個被引數化的型別。非泛型類可以從一個封閉結構的泛型類進行派生,但是不能從一個開放結構的泛型類派生。即:

public class MyClass : MyClass1<int>

是有效的, 但是

public class MyClass : MyClass1<T>

是無效的。

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可以工作的方法:

public void Package(Basket<Fruit> aBasket)
{
aBasket.Add(new Apple());
aBasket.Add(new Banana());
}

如果傳送一個Basket<Fruit>的例項給這個方法,這個方法將新增一個Apple物件和一個Banana物件。然而,傳送一個Basket<Apple>的例項給這個方法時,會是什麼情形呢?你看,這裡充滿技巧。這解釋了為什麼下列程式碼:

Basket<Apple> anAppleBasket = new Basket<Apple>();
Package(anAppleBasket);

會產生錯誤:

Error 2 Argument ’1’:
cannot convert from ’TestApp.Basket<testapp.apple>’
to ’TestApp.Basket<testapp.fruit>’

編譯器通過確保我們不會隨意地傳遞一個集合的派生類(此時需要一個集合的基類),保護了我們的程式碼。這不是很好嗎?

這在上面的例中在成功的,但也存在特殊情形:有時我們確實想傳遞一個集合的派生類,此時需要一個集合的基類。例如,考慮一下Animal(如Monkey),它有一個把Basket<Fruit>作引數的方法Eat,如下所示:

public void Eat(Basket<Fruit> fruits)
{
foreach (Fruit aFruit in fruits)
{
//將吃水果的程式碼
}
}

現在,你可以呼叫:

Basket<Fruit> fruitsBasket = new Basket<Fruit>();
… //新增到Basket物件中的物件Fruit
anAnimal.Eat(fruitsBasket);

如果你有一籃子(a Basket of)Banana-一Basket<Banana>,情況會是如何呢?把一籃子(a Basket of)Banana-一Basket<Banana>傳送給Eat方法有意義嗎?在這種情形下,會成功嗎?真是這樣的話,編譯器會給出錯誤資訊:

Basket<Banana> bananaBasket = new Basket<Banana>();
//…
anAnimal.Eat(bananaBasket);

編譯器在此保護了我們的程式碼。我們怎樣才能要求編譯器允許這種特殊情形呢?約束機制再一次幫助了我們:

public void Eat<t>(Basket<t> fruits) where T : Fruit
{
foreach (Fruit aFruit in fruits)
{
//將吃水果的程式碼
}
}

在建立方法Eat()的過程中,我要求編譯器允許一籃子(a Basket of)任何型別T,這裡T是Fruit型別或任何繼承自Fruit的類。

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;
}

我們已經引數化了代理和介面。現在的介面的實現中應該能確定這是一種什麼型別。

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中的泛型是強有力的,你寫的程式碼不必限定於一特定型別,然而你的程式碼卻能具有型別安全性。泛型的實現目標是既提高程式的效能又不造成程式碼的臃腫。然 而,在它的約束機制存在不足(無法指定一型別必須是另外一種型別的基型別)的同時,該約束機制也給你書寫程式碼帶來很大的靈活性,因為你不必拘泥於各種型別 的"最小公分母"能力。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-536623/,如需轉載,請註明出處,否則將追究法律責任。

相關文章