C#圖解教程 第十七章 泛型

泛型

什麼是泛型


到如今爲止,全部在類聲明中用到的類型都是特定的類型–或是程序員定義的,或是語言或BCL定義的。然而,不少時候,咱們須要把類的行爲提取或重構出來,使之不只能用到它們編碼的數據類型上,還能應用到其餘類型上。
泛型能夠作到這一點。咱們重構代碼並額外增長一個抽象層,對於這樣的代碼來講,數據類型就不用硬編碼了。這是專門爲多段代碼在不一樣的數據類型上執行相同指令的狀況專門設計的。程序員

聽起來比較抽象,下面看一個示例安全

一個棧的示例

假設咱們聲明一個MyIntStack類,該類實現一個int類型的棧。它容許int值的壓入彈出。函數

class MyIntStack
{
    int StackPointer=0;
    int[] StackArray;
    public void Push(int x)
    {
        ...
    }
    public int Pop()
    {
        ...
    }
}

假設如今但願將相同的功能應用與float類型的值,能夠有幾種方式來實現。不用泛型,按照咱們之前的思路產生的代碼以下。this

class MyFloatStack
{
    int StackPointer=0;
    float[] StackArray;
    public void Push(float x)
    {
        ...
    }
    public float Pop()
    {
        ...
    }
}

這個方法固然可行,但容易出錯且有以下缺點:編碼

  • 咱們須要仔細檢查類的每部分來看哪些類型的聲明須要修改,哪些須要保留
  • 每次須要新類型的棧類時,咱們須要重複該過程
  • 代碼冗餘
  • 不宜調試和維護

C#中的泛型


泛型(generic)特性提供了一種更優雅的方式,可讓多個類型共享一組代碼。泛型容許咱們聲明類型參數化(type-parameterized)的代碼,能夠用不一樣的類型進行實例化。即咱們能夠用「類型佔位符」來寫代碼,而後在建立類的實例時指明真實的類型。
本書讀到這裏,咱們應該很清楚類型不是對象而是對象的模板這個概念了。一樣地,泛型類型也不是類型,而是類型的模板

C#提供了5種泛型:類、結構、接口、委託和方法。
注意,前4個是類型,而方法是成員。 spa


繼續棧示例

將MyIntStack和MyFloatStack兩個類改成MyStack泛型類。設計

class MyStack<T>
{
    int StackPointer=0;
    T[] StackArray;
    public void Push(T x){...}
    public T Pop(){...}
}

泛型類


建立和使用常規的、非泛型的類有兩個步驟:聲明和建立類的實例。可是泛型類不是實際的類,而是類的模板,因此咱們必須從它們構建實際的類類型,而後建立實例。
下圖從一個較高的層面上演示了該過程。3d

  • 在某些類型上使用佔位符來聲明一個類
  • 爲佔位符提供真實類型。這樣就有了真實類的定義,填補了全部的「空缺」。該類型稱爲構造類型(constructed type)
  • 建立構造類型的實例

聲明泛型類


聲明一個簡單的泛型類和聲明普通類差很少,區別以下。調試

  • 在類名後放置一組尖括號
  • 在尖括號中用逗號分隔的佔位符字符串來表示但願提供的類型。這叫作類型參數(type parameter)
  • 在泛型類聲明的主體中使用類型參數來表示應該替代的類型
class SomeClass<T1,T2>
{
    public T1 SomeVar=new T1();
    public T2 OtherVar=new T2();
}

泛型類型聲明中沒有特殊的關鍵字,取而代之的是尖括號中的類型參數列表。code

建立構造類型


一旦建立了泛型類型,咱們就須要告訴編譯器能使用哪些真實類型來替代佔位符(類型參數)。
建立構造類型的語法以下,包括列出類名並在尖括號中提供真實類型來替代類型參數。要替代類型參數的真實類型叫作類型實參(type argument)。

SomeClass<short,int>

編譯器接受類型實參而且替換泛型類主體中的相應類型參數,產生構造類型–從它建立真實類型的實例。


下圖演示了類型參數和類型實參的區別。

  • 泛型類聲明上的類型參數用作類型的佔位符
  • 在建立構造類型時提供的真實類型是類型實參

建立變量和實例


在建立引用和實例方面,構造類類型的使用和常規類型類似。

MyNonGenClass myNGC=new MyNonGenClass();
SomeClass
<short,int> mySc1=new SomeClass<short,int>(); var mySc2=new SomeClass<short,int>();

和非泛型同樣,引用和實例能夠分開建立。

SomeClass<short,int> myInst;
myInst=new SomeClass<short,int>();

能夠從同一泛型類型構建不一樣類類型。每一個獨立的類類型,就好像它們都有獨立的非泛型類聲明同樣。

class SomeClass<T1,T2>
{
...
}
class Program
{
    static void Main()
    {
        var first=new SomeClass<short,int>();
        var second=new SomeClass<int,long>();
    }
}

使用泛型的棧的示例
class MyStack<T>
{
    T[] StackArray;
    int StackPointer=0;
    public void Push<T x>
    {
        if(!IsStackFull)
        {
            StackArray[StackPointer++]=x;
        }
    }
    public T Pop()
    {
        return (!IsStackEmpty)
            ?StackArray[--StackPointer]
            :StackArray[0];
    }
    const int MaxStack=10;
    bool IsStackFull{get{return StackPointer>=MaxStack;}}
    bool IsStackEmpty{get{return StackPointer<=0;}}
    public MyStack()
    {
        StackArray=new T[MaxStack];
    }
    public void Print()
    {
        for(int i=StackPointer-1;i>=0;i--)
        {
            Console.WriteLine("  Value:{0}",StackArray[i]);
        }
    }
}
class Program
{
    static void Main()
    {
        var StackInt=new MyStack<int>();
        var StackString=new MyStack<string>();
        StackInt.Push(3);
        StackInt.Push(5);
        StackInt.Push(7);
        StackInt.Push(9);
        StackInt.Print();
        StackString.Push("This is fun");
        StackString.Push("Hi there!  ");
        StackString.Print();
    }
}

比較泛型和非泛型棧

類型參數的約束


在泛型棧的示例中,棧除了保存和彈出它包含的一些項以外沒作任何事情。它不會嘗試添加、比較或作其餘任何須要用到項自己的運算符的事情。理由是,泛型棧不知道它保存的項的類型是什麼,也不知道這些類型實現的成員。
然而,C#對象都從object類繼承,所以,棧能夠確認,這些保存的項都實現了object類的成員。它們包括ToString、Equals以及GetType。
若是代碼嘗試使用除object類的其餘成員,編譯器會產生錯誤。

例:

class Simple<T>
{
    static public bool LessThan(T i1,T i2)
    {
        return i1<i2;      //錯誤
    }
    ...
}

要讓泛型變得更有用,咱們須要提供額外的信息讓編譯器知道參數能夠接受哪些類型。這些信息叫作約束(constrain)。只有符合約束的類型才能替代類型參數。

Where子句

約束使用Where子句列出。

  • 每一個約束的類型參數有本身的where子句
  • 若是形參有多個約束,它們在where子句中使用逗號分隔

where子句語法以下:

      類型參數         約束列表
         ↓               ↓
where TypeParam:constraint,constraint,...
  ↑            ↑
關鍵字         冒號

有關where子句的要點:

  • 它們在類型參數列表的關閉尖括號以後列出
  • 它們不是用逗號或其餘符號分隔
  • 它們次序任意
  • where是上下文關鍵字,能夠在其餘上下文中使用

例:where子句示例

class MyClass<T1,T2,T3>
              where T2:Customer
              where T3:IComparable
{
    ...
}
約束類型和次序

where子句能夠以任何次序列出。然而where子句中的約束必須有特定順序。

  • 最多隻能有一個主約束,如有則必須放第一位
  • 能夠有任意多的接口名約束
  • 如有構造函數約束,必須放最後

例:約束示例

class SortedList<S>
        where S:IComparable<S>{...}
class LinkedList<M,N>
        where M:IComparable<M>
        where N:ICloneable{...}
class MyDictionary<KeyType,ValueType>
        where KeyType:IEnumerable,
        new()              {...}

泛型方法


與其餘泛型不同,方法是成員,不是類型。泛型方法能夠在泛型和非泛型類以及結構和接口中聲明。


聲明泛型方法

泛型方法具備類型參數列表和可選的約束

  • 泛型方法有兩個參數列表
    • 封閉在圓括號內的方法參數列表
    • 封閉在尖括號內的類型參數列表
  • 要聲明泛型方法,須要:
    • 在方法名稱後和方法參數列表前放置類型參數列表
    • 在方法參數列表後放置可選的約束子句
                  類型參數列表      約束子句
                       ↓             ↓
public void PrintData<S,T>(S p,T t)where S:Person
{                             ↑
    ...                  方法參數列表
}

記住,類型參數列表在方法名稱後,在方法參數列表前。

調用泛型方法

調用方法,需在調用時提供類型實參,以下:

MyMethod<short,int>();
MyMethod<int,long>();

例:調用泛型方法示例


推斷類型

若是咱們爲方法傳入參數,編譯器有時能夠從方法參數中推斷出泛型方法的類型形參用到的那些類型。這樣就可使方法調用更簡單,可讀性更強。
以下代碼,若咱們使用int類型變量調用MyMethod,方法調用中的類型參數信息就多餘了,由於編譯器能夠從方法參數得知它是int。

int myInt=5;
MyMethod<int>(myInt);

因爲編譯器能夠從方法參數中推斷類型參數,咱們能夠省略類型參數和調用中的尖括號,以下:

MyMethod(myInt);
泛型方法示例
class Simple
{
    static public void ReverseAndPrint<T>(T[] arr)
    {
        Array.Reverse(arr);
        foreach(T item in arr)
        {
            Console.WriteLine("{0},",item.ToString());
        }
        Console.WriteLine("");
    }
}
class Program
{
    static void Main()
    {
        var intArray=new int[]{3,5,7,9,11};
        var stringArray=new string[]{"first","second","third"};
        var doubleArray=new double[]{3.567,7,891,2,345};
        Simple.ReverseAndPrint<int>(intArray);
        Simple.ReverseAndPrint(intArray);
        Simple.ReverseAndPrint<string>(stringArray);
        Simple.ReverseAndPrint(stringArray);
        Simple.ReverseAndPrint<double>(doubleArray);
        Simple.ReverseAndPrint(doubleArray);
    }
}

擴展方法和泛型類


在第7章中,咱們詳細介紹了擴展方法,它也能夠和泛型類結合使用。它容許咱們將類中的靜態方法關聯到不一樣的泛型類上,還容許咱們像調用類結構實例的實例方法同樣來調用方法。
和非泛型類同樣,泛型類的擴展方法:

  • 必須聲明爲static
  • 必須是靜態類的成員
  • 第一個參數類型中必須有關鍵字this,後面是擴展的泛型類的名字
static class ExtendHolder
{
    public static void Print<T>(this Holder<T>h)
    {
        T[] vals=h.GetValue();
        Console.WriteLine("{0},\t{1},\t{2}",vals[0],vals[1],vals[2]);
    }
}
class Holder<T>
{
    T[] Vals=new T[3];
    public Holder(T v0,T v1,T v2)
    {
        Vals[0]=v0;Vals[1]=v1;Vals[2]=v2;
        public T[] GetValues(){return Vals;}
    }
}
class Program
{
    static void Main()
    {
        var intHolder=new Holder<int>(3,5,7);
        var stringHolder=new Holder<string>("a1","b2","c3");
        intHolder.Print();
        stringHolder.Print();
    }
}

泛型結構


與泛型類類似,泛型結構能夠有類型參數和約束。泛型結構的規則和條件與泛型類同樣。

struct PieceOfData<T>
{
    public PieceOfData(T value){_data=value;}
    private T _data;
    public T Data
    {
        get{return _data;}
        set{_data=value;}
    }
}
class Program
{
    static void Main()
    {
        var intData=new PieceOfData<int>(10);
        var stringData=new PieceOfData<string>("Hi there.");
        Console.WriteLine("intData    ={0}",intData.Data);
        Console.WriteLine("stringData ={0}",stringData.Data);
    }
}

泛型委託


泛型委託與非泛型委託很是類似,不過類型參數決定能接受什麼樣的方法。

  • 要聲明泛型委託,在委託名稱後、委託參數列表前的尖括號中放置類型參數列表
  • `delegate R MyDelegate<T,R>(T Value);`
  • 注意,有兩個參數列表:委託形參列表和類型參數列表
  • 類型參數的範圍包括:
    • 返回值
    • 形參列表
    • 約束子句

例:泛型委託示例

delegate void MyDelegate<T>(T value);
class Simple
{
    static public void PrintString(string s)
    {
        Console.WriteLine(s);
    }
    static public void PrintUpperString(string s)
    {
        Console.WriteLine("{0}",s.ToUpper());
    }
}
class Program
{
    static void Main()
    {
        var myDel=new MyDelegate<string>(Simple.PrintString);
        myDel+=Simple.PrintUpperString;
        myDel("Hi There.");
    }
}

另外一個 泛型委託示例

C#的LINQ(第19章)特性在不少地方使用了泛型委託,但在介紹LINQ前,有必要給出另一個示例。

public delegate TR Func<T1,T2,TR>(T1 p1,T2 p2);//泛型委託
class Simple
{
    static public string PrintString(int p1,int p2)
    {
        int total=p1+p2;
        return total.ToString();
    }
}
class Program
{
    static void Main()
    {
        var myDel=new Fun<int,int,string>(Simple.PrintString);
        Console.WriteLine("Total:{0}",myDel(15,13));
    }
}

泛型接口


泛型接口容許咱們編寫參數和返回類型是泛型類型參數的接口。

例:IMyIfc泛型接口

interface IMyIfc<T>
{
    T ReturnIt(T inValue);
}
class Simple<S>:IMyIfc<S>
{
    public S ReturnIt(S inValue)
    {
        return inValue;
    }
}
class Program
{
    static void Main()
    {
        var trivInt=new Simple<int>();
        var trivString=new Simple<string>();
        Console.WriteLine("{0}",trivInt.ReturnIt(5));
        Console.WriteLine("{0}",trivString.ReturnIt("Hi there."));
    }
}

使用泛型接口的示例

以下示例演示了泛型接口的兩個額外能力:

  • 實現不一樣類型參數的泛型接口是不一樣的接口
  • 能夠在非泛型類型中實現泛型接口

例:Simple是實現泛型接口的非泛型類。

interface IMyIfc<T>
{
    T ReturnIt(T inValue);
}
class Simple:IMyIfc<int>,IMyIfc<string>     //非泛型類
{
    public int ReturnIt(int inValue)        //實現int類型接口
    {return inValue;}
    public string ReturnIt(string inValue)  //實現string類型接口
    {return inValue;}
}
class Program
{
    static void Main()
    {
        var trivial=new Simple();
        Console.WriteLine("{0}",trivial.ReturnIt(5));
        Console.WriteLine("{0}",trivial.ReturnIt("Hi there."));
    }
}
泛型接口的實現必須惟一

實現泛型類接口時,必須保證類型實參組合不會在類型中產生兩個重複的接口。

例:Simple類使用了兩個IMyIfc接口的實例化。
對於泛型接口,使用兩個相同接口自己沒有錯,但這樣會產生一個潛在衝突,由於若是把int做爲類型參數來替代第二個接口中的S的話,Simple可能會有兩個相同類型的接口,這是不容許的。

interface IMyIfc<T>
{
    T ReturnIt(T inValue);
}
class Simple<S>:IMyIfc<int>,IMyIfc<S>    //錯誤
{
    public int ReturnIt(int inValue)
    {return inValue;}
    public S ReturnIt(S inValue)   //若是它不是int類型的
    {return inValue;}              //將和上個示例的接口同樣
}

說明:泛型接口的名字不會和非泛型衝突。例如,在前面的代碼中咱們還能夠聲明一個名爲IMyIfc的非泛型接口。

協變


縱觀本章,你們已經看到,若是你建立泛型類型的實例,編譯器會接受泛型類型聲明以及類型參數來構造類型。可是,你們一般會錯誤的將派生類型分配給基類型的變量。下面咱們來看一下這個主題,這叫作可變性(variance)。它分爲三種–協變(convariance)、逆變(contravariance)和不變(invariance)。
首先回顧已學內容,每一個變量都有一種類型,能夠將派生類對象的實例賦值給基類變量,這叫賦值兼容性

例:賦值兼容性

class Animal
{
    public int NumberOfLegs=4;
}
class Dog:Animal
{
}
class Program
{
    static void Main()
    {
        var a1=new Animal();
        var a2=new Dog();
        Console.WriteLine("Number of dog legs:{0}",a2.NumberOfLegs);
    }
}

如今,咱們來看一個更有趣的例子,用下面的方式對代碼進行擴展。

  • 增長一個叫作Factory的泛型委託,它接受類型參數T,不接受方法參數,而後返回一個類型爲T的對象
  • 添加一個叫MakeDog的方法,不接受參數但返回一個Dog對象。若是咱們使用Dog做爲類型參數的話,這個方法能夠匹配Factory委託
class Animal{public int NumberOfLegs=4;}
class Dog:Animal{}
delegate T Factory<T>();
class Program
{
    static Dog MakeDog()
    {
        return new Dog();
    }
    static void Main()
    {
        Factory<Dog> dogMaker=MakeDog;
        Factory<Animal>animalMaker=dogMaker;
        Console.WriteLine(animalMaker().Legs.ToString());
    }
}

上面代碼在Main的第二行會報錯,編譯器提示:不能隱式把右邊的類型轉換爲左邊的類型。
看上去由派生類型構造的委託應該能夠賦值給由基類構造的委託,那編譯器爲什麼報錯?難道賦值兼容性原則不成立了?
不是,原則依然成立,可是對於這種狀況不適用!問題在於儘管Dog是Animal的派生類,可是委託Factory<Dog>沒有從委託Factory<Animal>派生。相反,兩個委託對象是同級的,它們都從delegate類型派生。

再仔細分析一下這種狀況,咱們能夠看到,若是類型參數只用做輸出值,則一樣的狀況也適用於任何泛型委託。對於全部這樣的狀況,咱們應該可使用由派生類建立的委託類型,這樣應該可以正常工做,由於調用代碼老是指望獲得一個基類的引用,這也正是它會獲得的。
若是派生類只是用於輸出值,那麼這種結構化的委託有效性之間的常數關係叫作協變。爲了讓編譯器知道這是咱們的指望,必須使用out關鍵字標記委託聲明中的類型參數。
增長out關鍵字後,代碼就能夠經過編譯並正常工做了。

delegate T Factory<out T>();
                    ↑
            關鍵字指定了類型參數的協變
  • 圖左邊棧中的變量是T Factory<out T>()的委託類型,其中類型變量T是Animal類
  • 圖右邊堆上實際構造的委託是使用Dog類類型變量進行聲明的,Dog從Animal派生
  • 這是可行的,儘管調用委託時,調用代碼接受Dog類型的對象,而不是指望的Animal類型對象,可是調用代碼能夠像以前指望的那樣自由地操做對象的Animal部分

逆變


如今來看另外一種狀況。

class Animal{public int NumberOfLegs=4;}
class Dog:Animal{}
delegate T Factory<T>();
class Program
{
    delegate void Action1<in T>(T a);
    static void ActOnAnimal(Animal a)
    {
        Console.WriteLine(a.NumberOfLegs);
    }
    static void Main()
    {
        Action1<Animal> act1=ActOnAnimal;
        Action1<Dog> dog1=act1;
        dog1(new Dog());
    }
}

和以前狀況類似,默認狀況下不能夠賦值兩種不兼容的類型。但在某些狀況下可讓這種賦值生效。
其實,若是類型參數只用做委託中方法的輸入參數的話就能夠了。由於即便調用代碼傳入了一個程度更高的派生類的引用,委託中的方法也只指望一個程度低一些的派生類的引用,固然,它也仍然接受並知道如何操做。
這種指望傳入基類時容許傳入派生對象的特性叫作逆變。能夠在類型參數中顯式使用in關鍵字來使用。

  • 圖左邊棧上的變量是void Action1<in T>(T p)類型的委託,其類型變量是Dog類
  • 圖右邊實際構建的委託使用Animal類的類型變量來聲明,它是Dog類的基類
  • 這樣能夠工做,由於在調用委託時,調用代碼爲方法ActOnAnimal傳入Dog類型的變量,而它指望的是Animal類型的對象。方法固然能夠像指望的那樣自由操做對象的Animal部分

下圖總結了泛型委託中協變和逆變的不一樣


  • 上面的圖演示了協變:
    • 左邊棧上的變量是F<out T>()類型的委託,類型變量是叫作Base的類
    • 在右邊實際構建的委託,使用Derived類的類型變量聲明,這個類派生自Base
    • 這樣能夠工做,由於在調用時,方法返回指向派生類型的對象的引用,派生類型一樣指向其基類,調用代碼可正常工做
  • 下面的圖演示了逆變:
    • 左邊棧上的變量是F<in T>(T p)類型的委託,類型參數是Derived類
    • 在右邊實際構建的委託,使用Base類的類型變量聲明,這個類是Derived類的基類
    • 這樣能夠工做,由於在調用時,調用代碼傳入了派生類型的變量,方法指望的只是其基類,方法徹底能夠像之前那樣操做對象的基類部分
接口的協變和逆變

如今你應該已經理解了協變和逆變能夠應用到委託上。其實相同的原則也可用到接口上,能夠在聲明接口的時候使用out和in關鍵字。

例:使用協變的接口

class Animal{public string Name;}
class Dog:Animal{};
interface IMyIfc<out T>
{
    T GetFirst();
}
class SimpleReturn<T>:IMyIfc<T>
{
    public T[] items=new T[2];
    public T GetFirst()
    {
        return items[0];
    }
}
class Program
{
    static void DoSomething(IMyIfc<Animal>returner)
    {
        Console.WriteLine(returner.GetFirst().Name);
    }
    static void Main()
    {
        SimpleReturn<Dog> dogReturner=new SimpleReturn<Dog>();
        dogReturner.items[0]=new Dog(){Name="Avonlea"};
        IMyIfc<Animal> animalReturner=dogReturner;
        DoSomething(dogReturner);
    }
}
有關可變性的更多內容

以前的兩小節解釋了顯式的協變和逆變。還有一些狀況編譯器能夠自動識別某個已構建的委託是協變或是逆變並自動進行類型強制轉換。這一般發生在沒有爲對象的類型賦值的時候,以下代碼演示了該例子。

class Animal{public int Legs=4;}
class Dog:Animal{}
class Program
{
    delegate T Factory<out T>();
    static Dog MakeDog()
    {
        return new Dog();
    }
    static void Main()
    {
        Factory<Animal> animalMaker1=MakeDog;//隱式強制轉換
        Factory<Dog> dogMaker=MakeDog;
        Factory<Animal> animalMaker2=dogMaker;//須要out標識符
        Factory<Animal> animalMaker3
                   =new Factory<Dog>(MakeDog);//須要out標識符
    }
}

有關可變性的其餘一些重要事項以下:

  • 變化處理的是使用派生類替換基類的安全狀況,反之亦然。所以變化只適用於引用類型,由於不能從值類型派生其餘類型
  • 顯式變化使用in和out關鍵字只適用於委託和接口,不適用於類、結構和方法
  • 不包括in和out關鍵字的委託和接口類型參數叫作不變。這些類型參數不能用於協變或逆變
                         協變
                          ↓
delegate T Factory<out R,in S,T>();
                     ↑        ↑
                    逆變     不變
相關文章
相關標籤/搜索