1. 程式人生 > >C#圖解教程 第十七章 泛型

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

泛型

什麼是泛型


到現在為止,所有在類宣告中用到的型別都是特定的型別–或是程式設計師定義的,或是語言或BCL定義的。然而,很多時候,我們需要把類的行為提取或重構出來,使之不僅能用到它們編碼的資料型別上,還能應用到其他型別上。 
泛型可以做到這一點。我們重構程式碼並額外增加一個抽象層,對於這樣的程式碼來說,資料型別就不用硬編碼了。這是專門為多段程式碼在不同的資料型別上執行相同指令的情況專門設計的。

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

一個棧的示例

假設我們宣告一個MyIntStack類,該類實現一個int型別的棧。它允許int值的壓入彈出。

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

假設現在希望將相同的功能應用與float型別的值,可以有幾種方式來實現。不用泛型,按照我們以前的思路產生的程式碼如下。

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

這個方法當然可行,但容易出錯且有如下缺點:

  • 我們需要仔細檢查類的每部分來看哪些型別的宣告需要修改,哪些需要保留
  • 每次需要新型別的棧類時,我們需要重複該過程
  • 程式碼冗餘
  • 不宜除錯和維護

C#中的泛型


泛型(generic)特性提供了一種更優雅的方式,可以讓多個型別共享一組程式碼。泛型允許我們宣告型別引數化(type-parameterized)的程式碼,可以用不同的型別進行例項化。即我們可以用“型別佔位符”來寫程式碼,然後在建立類的例項時指明真實的型別。 
本書讀到這裡,我們應該很清楚型別不是物件而是物件的模板這個概念了。同樣地,泛型型別也不是型別,而是型別的模板。 
 
C#提供了5種泛型:類、結構、介面、委託和方法。 
注意,前4個是型別,而方法是成員。


繼續棧示例

將MyIntStack和MyFloatStack兩個類改為MyStack泛型類。

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

泛型類


建立和使用常規的、非泛型的類有兩個步驟:宣告和建立類的例項。但是泛型類不是實際的類,而是類的模板,所以我們必須從它們構建實際的類型別,然後建立例項。 
下圖從一個較高的層面上演示了該過程。

  • 在某些型別上使用佔位符來宣告一個類
  • 為佔位符提供真實型別。這樣就有了真實類的定義,填補了所有的“空缺”。該型別稱為構造型別(constructed type)
  • 建立構造型別的例項

宣告泛型類


宣告一個簡單的泛型類和宣告普通類差不多,區別如下。

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

泛型型別宣告中沒有特殊的關鍵字,取而代之的是尖括號中的型別引數列表。

建立構造型別


一旦建立了泛型型別,我們就需要告訴編譯器能使用哪些真實型別來替代佔位符(型別引數)。 
建立構造型別的語法如下,包括列出類名並在尖括號中提供真實型別來替代型別引數。要替代型別引數的真實型別叫做型別實參(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>();
                     ↑        ↑
                    逆變     不變

from:http://www.cnblogs.com/moonache/p/6385258.html