重溫CLR(八 ) 泛型

  熟悉面向對象編程的開發人員都深諳面向對象的好處,其中一個好處是代碼重用,它極大提升了開發效率。也就是說,能夠派生出一個類,讓他繼承基類的全部能力。派生類只須要重寫虛方法,或添加一些新方法,就可定製派生類的行爲,使之知足開發人員的需求。泛型(generic)是clr和編程語言提供的一種特殊機制,它支持另外一種形式的代碼重用,即「算法重用」。node

       簡單地說,開發人員先定義好算法,好比排序、搜索、交換、比較或者轉換等。可是,定義算法的開發人員並不設定該算法要操做什麼數據類型。而後,另外一個開發人員只要制定了算法要操做的具體數據類型,就可使用算法了。算法

       大多數算法都封裝在一個類型中,clr容許建立泛型引用類型和泛型值類型,但不容許建立泛型枚舉類型。此外,clr還容許建立泛型接口和泛型委託。方法偶爾也封裝有用的算法,因此clr容許在引用類型、值類型或接口中定義泛型方法。編程

  泛型有兩種表現形式:泛型類型和泛型方法c#

泛型類型:大多數算法都封裝在一個類型中,CLR容許建立泛型引用類型和泛型值類型,但不容許建立泛型枚舉類型。除此以外,CLR還容許建立泛型接口和泛型委託。數組

泛型方法:方法偶爾也封裝有用的算法,因此CLR容許引用類型、值類型或接口中定義泛型方法。安全

  二者都是表示API的基本方法(無論是指一個泛型方法仍是一個完整的泛型類型),以至平時指望出現一個普通類型的地方出現一個類型參數。好比,List<T>,在類名以後添加一個<T>,代表它操做的是一個未指定的數據類型。定義泛型類型和方法時,它爲類型指定的任何變量(好比 T)都稱爲類型參數(type parameter)。T表明一個變量名,在源代碼中可以使用一個數據類型的任何位置 ,都能使用T。數據結構

  類型參數是真實類型的佔位符。在泛型聲明中,類型參數要放在一堆尖括號內,並以逗號分隔。因此,在Dictionary<TKey, TValue>中,類型參數是TKey和TValue。使用泛型類型或方法時,要使用真實的類型代替。這些真實的類型稱爲類型實參(type argument)。編程語言

泛型爲開發人員提供瞭如下優點:ide

1) 源代碼保護性能

       使用一個泛型算法的開發人員不須要訪問算法的源代碼。然而,使用C++模板的泛型技術時,算法的源代碼必須提供給準備使用算法的用戶。

2) 類型安全

  將一個泛型算法應用於一個具體的類型時,編譯器和CLR能理解開發人員的意圖,並保證只有與制定數據類型兼容的對象才能隨同算法使用。

3) 更清晰的代碼

  因爲編譯器強制類型安全性,因此減小了源代碼中必須進行的轉型次數。

4) 更佳的性能

  在有泛型以前,要想定義一個常規化的算法,它的全部成員都要定義成操做Object數據類型。這其中就要有裝箱和拆箱之間的性能損失。因爲如今能建立一個泛型算法來操做一個具體的值類型,因此值類型的實例能以傳值的方式傳遞,CLR再也不須要只需任何裝箱操做。因爲再也不須要轉型,因此CLR沒必要檢查嘗試一次轉型操做是否類型安全,一樣提升了代碼的容許速度。

fcl中的泛型

  泛型最明顯的應用就是集合類。FCL已經定義了幾個泛型集合類。其中大多數類能在Sysytem.Collections.Generic和System.Collections.ObjectModel命名空間中。要使用線程安全的泛型集合類,能夠去System.Collections.Concurrent命名空間尋找。

  Microsoft建議開發人員使用泛型集合類,並基於幾個方面的緣由,不鼓勵使用非泛型集合類(arrayList之類)。首先,非泛型沒法得到類型安全性、更清晰的代碼和更佳的性能。其次,泛型具備更好的對象模型。

  集合類實現了許多接口,放入集合中的對象也可能實現了接口,集合類可利用這些接口執行像排序這樣的操做。FCL內建了許多泛型接口定義,因此在使用接口時,也能體會到泛型帶來的好處。經常使用的接口包含在Sysytem.Collections.Generic命名空間中。

  System.Array類(即全部數組的基類)提供了大量靜態泛型方法,好比,AsReadonly、FindAll、Find、FindIndex等。

 

泛型基礎結構

       泛型在clr2.0中加入。爲了在clr加入泛型,許多人花費了大量時間來完成這個任務,好比如下

1 建立新的il命令,使之可以識別類型實參

2 修改現有元數據表的格式,以便表示具備泛型參數的類型和方法

3 修改編程語言以支持新語法

4 修改編譯器,使之能生成新的il指令和修改的元數據格式

5 建立新的反射成員,是開發人員能查詢類型和成員

       如今,讓咱們一塊兒討論clr內部如何處理泛型。

開放類型和封閉類型

       咱們討論了clr如何爲應用程序使用的各類類型建立稱爲類型對象(type object)的內部數據結構。具備泛型類型參數的類型仍然是類型,clr一樣會爲它建立內部的類型對象。這一點適合引用類型(類)、值類型(結構)、接口類型和委託類型。然而,具備泛型類型參數的類型稱爲開放類型,clr禁止構造開放類型的任何實例。這相似於clr禁止構造接口類型的實例。

       代碼引用泛型類型時可指定一組泛型類型實參。爲全部類型參數都傳遞了實際的數據類型,類型就稱爲封閉類型。clr容許構造封閉類型的實例。然而,代碼引用泛型類型的時候,可能留下一些泛型類型實參未指定。這會在clr中建立新的開放類型對象,並且不能建立該類型的實例。以下例子

// A partially specified open type
internal sealed class DictionaryStringKey<TValue> :
    Dictionary<String, TValue>
{
}
class Program
{
    private static void Main(string[] args)
    {
        Object o = null;

        // Dictionary<,> 是一個開放類型,有兩個類型參數
        Type t = typeof(Dictionary<,>);

        // 嘗試建立該類型的一個實例 (失敗)
        o = CreateInstance(t);
        Console.WriteLine();

        // DictionaryStringKey<> 是一個開放類型,有一個類型參數
        t = typeof(DictionaryStringKey<>);

        // 嘗試建立該類型的一個實例 (失敗)
        o = CreateInstance(t);
        Console.WriteLine();

        // DictionaryStringKey<Guid> 是一個封閉類型
        t = typeof(DictionaryStringKey<Guid>);

        // 嘗試建立該類型的一個實例 (成功)
        o = CreateInstance(t);

        // Prove it actually worked
        Console.WriteLine("Object type=" + o.GetType());

        Console.ReadKey();
    }

    private static Object CreateInstance(Type t)
    {
        Object o = null;
        try
        {
            o = Activator.CreateInstance(t);
            Console.Write("已建立 {0} 的實例", t.ToString());
        }
        catch (ArgumentException e)
        {
            Console.WriteLine(e.Message);
        }
        return o;
    }
}

  還要注意的是,CLR會在類型對象內部分配類型的靜態字段。所以,每一個封閉類型都有本身的靜態字段。換言之,假如List<T>定義了任何靜態字段,這些字段不會在一個List<DataTime>和List<String>之間共享;每一個封閉類型對象都有它本身的靜態字段。另外,假如一個泛型類型定義了一個靜態構造器,那麼針對每一個封閉類型,這個構造器都會執行一次。在泛型類型上定義一個靜態構造器的目的是保證傳遞的類型參數知足特定的條件。例如,若是但願一個泛型類型值用於處理枚舉類型,能夠以下定義:

internal sealed calss GenericTypeThatReqiresAnEnum<T> {
    static GenericTypeThatReqiresAnEnum() {
        if ( !typeof (T).IsEnum) {
            throw new ArgumentException("T must be an enumerated type")
        }
    }
}

CLR提供了一個名爲"約束"(constraint)的功能,可利用它更好地定義一個泛型類型來指出哪一個類型實參是有效的。

泛型類型和繼承

       泛型類型仍然是類型,因此它能從其餘任何類型派生。使用一個泛型類型並指定類型實參時,其實是在CLR中定義一個新的類型對象,新的類型對象是從派生該泛型類型的那個類型派生的。也就是說,因爲List<T>是從Object派生的,那麼List<String>和List<Guid>也是從Object派生的。

       相似地,因爲DictionaryStringKey<TValue>從Dictionary<String,TValue>派生,因此DictionaryStringKey<Guid>也從Dictionary<String, Guid >派生。指定類型實參不影響繼承層次結構。

       定義每一個節點爲具體數據類型的鏈表,比較好的辦法是定義非泛型Node基類,再定義泛型TypedNode類(用node類做爲基類)。這樣每一個節點都是一種具體的數據類型,同時得到編譯時的類型安全性,並防止值類型裝箱。

class Node
{
    protected Node m_next;
    public Node(Node next)
    {
        m_next = next;
    }
}
internal sealed class TypedNode<T>:Node
{
    public T m_data;
    public TypedNode(T data):this(data,null){}
    public TypedNode(T data,Node next):base(next)
    {
        m_data = data;
    }
    public override string ToString()
    {
        return m_data + (m_next != null ? m_next.ToString() : string.Empty);
    }
}

 

泛型類型同一性

       泛型語法有時會將開發人員弄糊塗,由於源代碼中可能散佈着大量「<」和」 >」符號,這有損可讀性。爲了對語法進行加強,有的開發人員定義了一個新的非泛型類型,它從一個泛型類型派生,並制定了全部類型實參。

List<DateTime> dt = new List<DateTime>();

一些開發人員可能首先定義下面這樣的一個類:

internal sealed class DateTimeList : List<DataTime> {
        //這裏無需聽任何代碼!
}

而後就能夠簡化建立列表的代碼

DateTimeList  dt = new DateTimeList ();

這樣作表面上是方便了,可是絕對不要單純出於加強源代碼的易讀性類這樣定義一個新類。這樣會喪失類型同一性(identity)和相等性(equivalence)。以下:

Boolean sameType = (typeof(List<DateTime>) == (typeof(DateTimeList));

  上述代碼運行時,sameType會初始化爲false,由於比較的是兩個不一樣類型的對象。也就是說,假如一個方法的原型接受一個DateTimeList,那麼不能將一個List<DateTime>傳給它。然而,若是方法的原型接受一個List<DateTime>,那麼能夠將一個DateTimeList傳給它,由於DateTimeList是從List<DateTime>派生的

  幸虧,C#提供一種方式,容許使用簡化的語法來引用一個泛型封閉類型,同時不會影響類的相等性——使用using指令。好比:

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

       using指令實際定義的是名爲DateTimeList的符號。

如今只想下面這行代碼時,sameType會初始化爲true:

Boolean sameType = (type(List<DateTime>) == (ypeof(DateTimeList));

還有,可使用C#的隱式類型局部變量功能,讓編譯器根據表達式的類型來推斷一個方法的局部變量的類型。

代碼爆炸

  使用泛型類型參數的一個方法在進行JIT編譯時,CLR獲取方法的IL,用指定的類型實參進行替換,而後建立恰當的本地代碼。然而,這樣作有一個缺點:CLR要爲每種不一樣的方法/類型組合生成本地代碼。咱們將這個現象稱爲"代碼爆炸"。它可能形成引用程序集的顯著增大,從而影響性能。

  CLR內建了一些優化措施,能緩解代碼爆炸。首先,假如爲一個特定的類型實參調用了一個方法,之後再次使用相同的類型實參來調用這個方法,CLR只會爲這個方法/類型組合編譯一次。因此,若是一個程序集使用List<DateTime>,一個徹底不一樣的程序集也使用List<DateTime>,CLR只會爲List<DateTime>編譯一次方法。

       CLR還提供了一個優化措施,它認爲全部引用類型實參都是徹底相同的,因此代碼可以共享。例如,clr爲List<String>的方法編譯的代碼可直接用於List<Stream>的方法,由於string和stream均爲引用類型。事實上,對於任何引用類型,都會使用相同的代碼。clr之因此能執行這個優化,是由於全部引用類型的實參或變量實際只是指向堆上對象的指針,而全部對象執政都以相同方式操做。

  可是,假如某個類型實參是值類型,CLR就必須專門爲那個值類型生成本地代碼。由於值類型的大小不定。即便類型、大小相同,CLR仍然沒法共享代碼,可能須要用不一樣的本地CPU指令操做這些值

泛型接口

       顯然,泛型的主要做用是定義泛型的引用類型和值類型。然而,對泛型接口的支持對clr來講也很重要。沒有泛型接口,每次用非泛型接口(如IComparable)來操縱值類型都會發生裝箱,並且會時區編譯時的類型安全性。這將嚴重製約泛型類型的應用方位。所以clr提供了對泛型接口的的和支持。引用類型或值類型可指定類型實參實現泛型接口。也可保持類型實參的未指定狀態來實現泛型接口。

如下是泛型接口定義是FCL的一部分:

public interface IEnumerator<T> : IDisposable, IEnumerator{
    T Current { get; }
}

下面的示例類型實現上述泛型接口,並且指定了類型實參。

internal sealed class Triangle : IEnumerator<Point> {
    private Point[] m_Vertice;
    public Point Current { get { ... }  } 
}

下面實現了相同的泛型接口,但保持類型實參的未指定狀態:

internal sealed class ArrayEnumerator<T> :  IEnumerator<T> {
    private T[] m_Vertice;
    public TCurrent { get { ... }  } 
}

       注意,arrayEnumerator對象可枚舉一組T對象。還要注意,current屬性如今具備未指定的數據類型T。

泛型委託

       CLR支持泛型委託,目的仍是保證任何類型的對象都能以類型安全的方式傳給回調方法。此外,泛型委託容許值類型實例在傳給回調方法時不進行任何裝箱。委託實際只是提供了4個方法的一個類定義。4個方法包括一個構造器、一個Invoke方法,一個BeginInvoke方法和一個EndInvoke方法。若是定義的委託類型制定了類型參數,編譯器會定義委託類的方法,用指定的類型參數替換方法的參數類型和返回值類型。

例如,假定向下面這樣定義一個泛型委託:

 public delegate TReturn CallMe<TReturn, TKey, TValue>(TKey key, TValue value);

編譯器會將它轉化成一個類,該類在邏輯上能夠這樣表示:

public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate {
    public CallMe(Object object, IntPtr method);
    public virtual TReturn Invoke(TKey key, TValue value);
    public virtual IAsycResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object);
    public virtual TReturn EndInvoke(IAsycResult  result);
}

建議儘可能使用在FCL中預約義的泛型Action和Func委託。

委託和接口的逆變和協變泛型類型實參

       委託的每一個泛型類型參數均可標記爲協變量或逆變量。利用這個功能,可將泛型委託類型的變量轉換爲相同的委託類型(但泛型參數類型不一樣)。泛型類型參數能夠是如下任何一種形式。

1 不變量(invariant)  意味着泛型類型參數不能更改。到目前爲止,你在本質看到的全是不變量形式的泛型類型參數。

2 逆變量(contravariant) 意味着泛型類型參數能夠從一個類型更改成它的某個派生類。在c#使用in 關鍵字標記逆變量形式的泛型類型參數。逆變量泛型類型參數只出如今輸入位置,好比做爲方法的參數。

3 協變量(covariant) 意味着泛型類型參數能夠從一個類更改成它的某個基類,c#使用out關鍵字標記協變量是行的泛型類型參數。協變量泛型參數只能出如今輸出位置,好比做爲方法的返回類型。

例如,如今存在如下委託類型定義(它在FCL中是存在的)

public delegate TResult Func<in T, Out TResult>(T arg);

其中,泛型類型參數T用in關鍵字標記,這使它成爲一個逆變量;泛型類型參數TResulr則用out關鍵字標記,這是它成爲一個協變量。

因此,若是像下面這樣聲明一個變量:

Func<Object,ArgumenException> fn1 = null;

就能夠將它轉型爲另外一個泛型類型參數不一樣的Func類型:

Func<String,Exception> fn2 = fn1;    //不須要顯示轉型

Exception e = fn("");

  使用要獲取泛型參數和返回值的委託時,建議儘可能爲逆變性和協變性指定in和out關鍵字。這樣作不會有不良反應,並使你的委託能在更多的情形中使用。

  和委託類似,具備泛型類型參數的接口也可將它的類型參數標記爲逆變量和協變量。好比:

public interface IEnumerator<out T> : IEnumerator {
    Boolean MoveNext();
    T Current{ get; }
}

因爲T是逆變量,因此如下代碼能夠順利編譯:

//這個方法接受任意引用類型的一個IEnumerable
Int32 Count(IEnumerable<Object> collection) { ... }
//如下調用向Count傳遞一個IEnumerable<String>
Int32 c = Count(new[] { "Grant" }); 

泛型方法

       定義泛型類、結構或接口時,這些類型中定義的任何方法均可引用由類型指定的一個類型參數。類型參數能夠做爲方法的參數,做爲方法的返回值,或者做爲方法內部定義的一個局部變量來使用。然而,CLR還容許方法指定它獨有的類型參數。這些類型參數可用於參數、返回值或者局部變量的類型使用

在下面的例子中,一個類型定義了一個類型參數,一個方法則定義了它本身的專用類型參數:

internal sealed class GenericType<T> {
    privete T m_value;
    public GenericType(T value) { m_value = value; }
    public TOutput Converter<TOutput>() {
        TOutput resulr= (TOurput) Convert.ChangeType(m_value,typeof(TOutput));
        return result;
    }
}

在這個例子中,GenericType類定義了類型參數(T),Converter方法也定義了本身的類型參數(TOutput)。這樣的GenericType能夠處理任意類型。Converter方法能將m_value字段引用的對象轉換成任意類型—具體取決於調用時傳遞的類型實參是什麼。泛型方法的存在,爲開發人員提供了極大的靈活性。

       泛型方法的一個很好的例子是swap方法:

private static void Swap<T>(ref T o1,ref T o2){
    T temp =o1;
    o1=o2;
    o2=temp;
}

泛型方法和類型推斷

  c#泛型語法由於涉及大量<>符號,因此開發人員很容易被弄得暈頭轉向。爲了改進代碼的建立,同事加強可讀性和維護性,C#編譯器支持在調用一個泛型方法時進行類型推斷(type inference)。這意味着編譯器會在調用一個泛型方法時自動判斷出要使用的類型。

private static void CallingSwapUsingInference() {
    Int32 n1 = 1, n2 = 2;
    Swap(ref n1, ref n2);    //調用Swap<Int32>
    String s1 = "A";
    Object s2 = "B";
    Swap(ref s1, ref s2);    //錯誤,不能推斷類型
}

  執行類型推斷時,C#使用變量的數據類型,而不是由變量引用的對象的實際類型。因此第二個swap調用中,c#發現s1是string,而s2是object(即便它剛好引用一個string)。因爲s1和s2是不一樣數據類型的變量,編譯器拿不許要爲swap傳遞什麼類型實參,因此會報錯。

       類型可定義多個方法,讓其中一個方法接收具體數據類型,讓另外一個接收泛型類型參數,以下例所示

private static void Display(string s){
   console.writeline(s)
}
private static void Display<T>(T o){
   Display(o.tostring()); 
}

下面展現了display方法的一些調用方式

display(「jeff」);     //調用Display(string s)
display(123);      //調用Display<T>(T o)
display<string>(「asdasd」);   //調用Display<T>(T o)

       在第一個調用中,編譯器可調用接收string參數的display方法,也可調用泛型display方法。但c#編譯器的策略是先考慮較明確的匹配,再考慮泛型匹配。對於第二個調用,編譯器不能調用接收string參數的非泛型方法,因此必須調用泛型方法。

       對於第三個調用,明確制定了泛型類型實參string。這告訴編譯器不要嘗試推斷類型實參。相反,應使用顯式指定的類型實參。這個例子中,編譯器會假定我想調用泛型方法,因此會調用泛型方法。

泛型和其餘成員

       在c#中,屬性、索引器、事件、操做符方法、構造器和終結器自己不能有類型參數。但它們能在泛型類型中定義,並且這些成員的代碼能使用類型的類型參數。

       c#之因此不容許這些成員指定本身的泛型類型參數,是由於Microsoft C#團隊認爲開發人員不多須要將這些成員做爲泛型使用。除此以外,爲這些成員添加泛型支持的代價是至關高的,由於必須爲語言設計足夠的語法。

 

可驗證性和約束

編譯泛型代碼時,c# 會進行分析,確保代碼適用於當前已有或未來可能定義的任何類型。看看下面方法

private static Boolean MethodTakingAnyType<T>(T o)
{
    T temp = o;
    Console.WriteLine(o.ToString());
    bool b = temp.Equals(o);
    return b;
}

       這個方法聲明瞭T類型的臨時變量(temp)。而後,方法執行兩次變量賦值和幾回方法調用。這個方法適用於任何類型。不管T是引用類型,值類型或枚舉類型,仍是接口或委託類型,它都能工做。

在看看下面方法

public static T Min<T>(T o1, T o2) 
{
    if (o1.CompareTo(o2)<0) return o1;
    return o2;
}

       min方法試圖使用o1變量來調用CompareTo方法。可是,許多類型都沒有提供CompareTo方法,因此c#編譯器不能編譯上述代碼,它不能保證這個方法適用於全部類型。

       因此從表面上看,使用泛型彷佛作不了太多事情。只能聲明泛型類型的變量,執行變量賦值,再調用Object定義的方法,如此而已!顯然,加入泛型只能這麼用,能夠說它幾乎沒有任何用。幸虧,編譯器和clr支持稱爲約束的機制,可經過它使泛型變得真正有用!

       約束的做用是限制能指定成泛型實參的類型數量。經過限制類型的數量,能夠對那些類型執行更多操做。如下如下仍是新版本的min方法,他指定了一個約束。

public static T Min<T>(T o1, T o2) where T : IComparable<T> {
         if (o1.CompareTo(o2)<0) return o1;
         return o2;
     }

       c#的wheer關鍵字告訴編譯器,爲T指定的任何類型都必須實現同類型(T)的泛型IComparable接口。有了這個約束,就能夠在方法中調用CompareTo,由於已知IComparable<T>接口定義了CompareTo。

       如今,當代碼引用泛型類型或方法時,編譯器要負責保證類型實參複合指定的約束,若是不符合約束,編譯器會報錯。

約束可應用於一個泛型類型的類型參數,也可應用於一個泛型方法的類型參數(就像Min所展現的)。CLR不容許基於類型參數名稱或約束來進行重載;只能基於元數(類型參數的個數)對類型或方法進行重載。下例對此進行了演示

internal sealed class AType { }
internal sealed class AType<T> { }
internal sealed class AType<T1, T2> { }

// 錯誤: 與沒有約束的 AType<T> 起衝突
internal sealed class AType<T> where T : IComparable<T> { }

// 錯誤: 與 AType<T1, T2> 起衝突
internal sealed class AType<T3, T4> { }

internal sealed class AnotherType 
{
   // 能夠定義一下方法,參數個數不一樣:
   private static void M() { }
   private static void M<T>() { }
   private static void M<T1, T2>() { }

   // 錯誤: 與沒有約束的 M<T> 起衝突
   private static void M<T>() where T : IComparable<T> { }

   // 錯誤: 與 M<T1, T2> 起衝突
   private static void M<T3, T4>() { }
}

  重寫虛泛型方法時,重寫的方法必須指定相同數量的類型參數,並且這些類型參數會繼承在基類方法上指定的約束。事實上,根本不容許爲重寫方法的類型參數指定任何約束。可是,類型參數的名稱是能夠改變的。相似的,實現一個接口方法時,方法必須指定與接口方法等量的類型參數,這些類型參數將繼承由接口的方法在它們前面指定的約束。下例使用虛方法演示了這一規則:

internal class Base {
   public virtual void M<T1, T2>()
      where T1 : struct
      where T2 : class {
   }
}
internal sealed class Derived : Base {
   public override void M<T3, T4>()
      where T3 : struct
      where T4 : class 
   {
   }
}

       試圖編譯上述代碼,編譯器會報告如下錯誤:

error CS0460:重寫和顯示接口實現方法的約束是從基方法繼承的,所以不能直接指定這些約束。

       註釋掉子類的約束,便可正常編譯。下面討論編譯器\clr容許向類型參數應用的各類約束。可用一個主要約束、一個次要約束以及一個構造器約束來約束類型參數。

主要約束

       類型參數能夠指定零個或者一個主要約束。主要約束能夠是表明未密封類的一個引用類型。不能指定如下特殊引用類型:System.Object,System.Array,System.Delagate,System.MulticastDelegate,System.ValueType,System.Enum和System.Void。

       指定引用類型約束時,至關於向編譯器承諾:一個指定的類型實參要麼是與約束類型相同的類型,要麼是從約束類型派生的類型。例如如下泛型類:

internal static class PrimaryConstraintOfStream<T> where T : Stream 
{
   public static void M(T stream) {
      stream.Close();   // OK
   }
}

       在這個類定義中,類型參數t設置了主要約束Stream(在system.IO命名空間中定義)。這就告訴編譯器,使用primaryConstraintOfStream的代碼在指定類型實參中,必須指定stream或者從stream派生的類型。若是類型參數沒有指定主要約束,就默認爲system.object。

       有兩個特殊的主要約束:class和struct。其中,class約束向編譯器承諾類型實參是引用類型。任何類類型、接口類型、委託類型或者數組類型都知足這個約束。例如如下泛型類

internal static class PrimaryConstraintOfClass<T> where T : class 
{
   public static void M() {
      T temp = null;    // 容許,T爲引用類型
   }
}

       在這個例子中,將temp設爲null是合法的,由於T已知是引用類型,而全部引用類型的變量都能設爲null。不對T進行約束,上述代碼就通不過編譯,由於T多是值類型,而值類型的變量不能設爲null。

       struct約束向編譯器承諾類型實參是值類型。包括枚舉在內的任何值類型都知足這個約束。但編譯器和clr將任何system.nullable<T>值類型視爲特殊類型,不知足這個struct約束。緣由是nullable<T>類型將它的類型參數約束爲struct,而clr但願禁止向nullable< nullable<T>> 這樣的遞歸類型。

internal static class PrimaryConstraintOfStruct<T> where T : struct 
{
   public static T Factory() {
      // 容許,由於值類型都有一個隱式無參構造器
      return new T();
   }
}

       這個例子中的new T()是合法的,由於T已知是值類型,而全部值類型都隱式地有一個公共無參構造器。若是T不約束,約束爲引用類型,或者約束爲class,上述代碼將沒法經過編譯,由於有的引用類型沒有公共無參構造器。

次要約束

       類型參數能夠指定零個或者多個次要約束,次要約束表明接口類型。這種約束向編譯器承諾類型實參實現了接口。因爲能指定多個接口約束,因此類型實參必須實現了全部接口約束(以及主要約束,若是有的話)。

       還有一種次要約束稱爲類型參數約束,有時也成爲裸類型約束。這種約束用的比接口約束少得多。它容許一個泛型類型或方法規定:指定的類型實參要麼就是約束的類型,要麼是約束的類型的派生類。一個類型參數能夠指定零個或多個類型參數約束。

 

private static List<TBase> ConvertIList<T, TBase>(IList<T> list)
   where T : TBase
{
    List<TBase> baseList = new List<TBase>(list.Count);
    for (Int32 index = 0; index < list.Count; index++)
    {
        baseList.Add(list[index]);
    }
    return baseList;
}

       ConvertIList方法制定了兩個類型參數,其中T參數由TBase類型參數約束。意味着無論爲T指定書目類型實參,都必須兼容與爲TBase指定的類型實參。下面這個方法演示了對ConvertIList的合法調用和非法調用:

private static void CallingConvertIList()
{
    //構造並初始化一個List<String>(它實現了IList<String>)
    IList<String> ls = new List<String>();
    ls.Add("A String");

    // 將IList<String>轉換成IList<Object>
    IList<Object> lo = ConvertIList<String, Object>(ls);

    // 將IList<String>轉換成IList<IComparable>
    IList<IComparable> lc = ConvertIList<String, IComparable>(ls);

    // 將IList<String>轉換成IList<IComparable<String>>
    IList<IComparable<String>> lcs =
       ConvertIList<String, IComparable<String>>(ls);

    // 將IList<String>轉換成IList<Exception>
    //IList<Exception> le = ConvertIList<String, Exception>(ls);    // 錯誤
}

構造器約束

類型參數能夠指定零個或者一個構造器約束。它向編譯器承諾類型實參是實現了公共無參構造器的非抽象類型。注意,若是同時使用構造器約束和struct約束,c#編譯器會認爲這是一個錯誤,由於這是多餘的;全部值類型都隱式提供了公共無參構造器。

internal sealed class ConstructorConstraint<T> where T : new()
{
    public static T Factory()
    {
        // 容許,由於值類型都有隱式無參構造器
        // 而約束要求任何引用類型也要有一個無參構造器
        return new T();
    }
}

       這個例子中的new T()是合法的,由於已知t是擁有公共無參構造器的類型。對全部值類型來講,這一點確定成立。對於做爲類型實參指定的任何引用類型,這一點也成立,由於構造器約束要求它必須成立。

       開發人員有時想爲類型參數指定一個構造器約束,並指定構造器要獲取多個參數。目前,clr(以及c#編譯器)只支持無參構造器。Microsoft認爲這已經能知足幾乎全部狀況,我對此也表示贊成。

其餘可驗證性問題

       本節剩下部分將討論幾個特殊的代碼構造。因爲可驗證性問題,這些代碼構造在和泛型共同使用時,可能產生不可預期的行爲。另外,還討論瞭如何利用約束使代碼從新變得能夠驗證。

1) 泛型類型變量的轉型

將一個泛型類型的變量轉型爲另外一個類型是非法的,除非將其轉型爲與一個約束兼容的類型:

private void CastingAGenericTypeVariable1<T>(T obj)
{
    Int32 x = (Int32)obj;    // 錯誤
    String s = (String)obj;  // 錯誤
}

上述兩行錯誤是由於T能夠是任何任何類型,沒法保證成功。爲了修改上述代碼使其能經過編譯,能夠先轉型爲object:

private void CastingAGenericTypeVariable2<T>(T obj)
{
      Int32 x = (Int32)(Object)obj;    // 不報錯
      String s = (String)(Object)obj;  // 不報錯
}

  如今雖然能編譯經過,但運行時也沒法保證是正確的。

  轉型爲引用類型時還可使用c# is或者as操做符。

2) 將一個泛型類型變量設爲默認值

將泛型類型變量設爲null是非法的,除非將泛型類型約束成引用類型。

private void SettingAGenericTypeVariableToNull<T>()
{
    //T temp = null;    // 錯誤, 值類型不能設置爲null,可考慮使用default('T')
}

因爲未對T進行約束,因此它多是值類型,而將值類型的變量設爲null是不可能的。若是T被約束成引用類型,將temp設爲null就是合法的,代碼能順利編譯並運行。

       c#團隊認爲有必要容許開發人員將變量設爲它的默認值,並專門爲此提供了default關鍵字

private void SettingAGenericTypeVariableToDefaultValue<T>()
{
    T temp = default(T);    // 正確
}

       以上代碼的default關鍵字告訴c#編譯器和clr的jit編譯器,若是t是引用類型,就將temp設爲null;若是是值類型,就將temp的全部位設爲0.

3) 將一個泛型類型變量與null進行比較

不管泛型類型是否非約束,使用==或!=操做符將一個泛型類型變量與null進行比較都是合法的。

private void ComparingAGenericTypeVariableWithNull<T>(T obj)
{
     if (obj == null) { /* 對值類型來講,永遠不會執行 */ }
}

       調用這個方法時,若是爲類型參數傳遞值類型,那麼jit編譯器知道if語句永遠都不會爲true,因此不會爲if測試或者大括號內的代碼生成本機代碼。

  若是T被約束成一個struct,C#編譯器會報錯。值類型的變量不能與null進行比較,由於結果始終同樣。

4)兩個泛型類型變量相互比較

若是泛型類型參數不是一個引用類型,對同一個泛型類型的兩個變量進行比較是非法的:

private void ComparingTwoGenericTypeVariables<T>(T o1, T o2)
{
      //if (o1 == o2) { }    // 錯誤
}

       在這個例子中,t未進行約束。雖然兩個引用類型的變量相互比較是合法的,但兩個值類型的變量相互比較是非法的,除非值類型重載了==操做符。若是t被約束城class,上述代碼能經過編譯。

       寫代碼比較基元類型時,c#編譯器知道如何生成正確的代碼。然而,對非基元值類型,c#編譯器不知道如何生成代碼進行比較。因此,若是ComparingTwoGenericTypeVariables方法被約束成struct,編譯器會報錯。

       不容許將類型參數約束成具體的值類型,由於值類型隱式密封,不可能存在從值類型派生的類型。若是容許將類型參數約束成具體的值類型,那麼泛型方法會被約束爲只支持該具體類型,這還不如不用泛型。

 

5)泛型類型變量做爲操做書使用

       最後要注意,將操做符應用於泛型類型的操做數會出現大量問題。在基元類型的那篇文章中,咱們指出c#知道如何解釋應用於基元類型的操做符(好比+,-)。但不能將這些操做符應用於泛型類型的變量。編譯器在編譯時肯定不了類型,因此不能向泛型類型的變量引用任何操做符。因此,不可能寫出一個能處理任何數值數據數據類型的算法。

private T Sum<T>(T num) where T : struct {
   T sum = default(T);
   for (T n = default(T); n < num; n++)
      sum += n;
   return sum;
}

       我想方設法想讓這個方法經過編譯。我將T約束成一個struct,並且將default(T)和sum和n初始化爲0。但編譯時獲得如下錯誤:

error  cs0019 運算符「<」沒法應用於T和T類型的操做數

error  cs0019 運算符「+=」沒法應用於T和T類型的操做數

       這是clr的泛型支持體系的一個嚴重限制,許多開發人員(尤爲是科學、金融和數學領域的開發人員)對這個限制感到很失望。許多人嘗試使用各類技術避開這個限制,其中包括反射、dynamic基元類型和操做符重載。但全部這些技術都會嚴重損害性能或者影響代碼可讀性。

相關文章
相關標籤/搜索