C#4.0新增功能03 泛型中的協變和逆變

協變逆變都是術語,前者指可以使用比原始指定的派生類型的派生程度更大(更具體的)的類型,後者指可以使用比原始指定的派生類型的派生程度更小(不太具體的)的類型。泛型類型參數支持協變和逆變,可在分配和使用泛型類型方面提供更大的靈活性。 在引用類型系統時,協變、逆變和不變性具備以下定義。 這些示例假定一個名爲 Base 的基類和一個名爲 Derived的派生類。html

  • Covariance編程

    使你可以使用比原始指定的類型派生程度更大的類型。api

    你能夠向 IEnumerable<Derived> 類型的變量分配IEnumerable(Of Derived) (在 Visual Basic 中爲 IEnumerable<Base>)的實例。安全

  • Contravarianceapp

    使你可以使用比原始指定的類型更泛型(派生程度更小)的類型。ide

    你能夠向 Action<Base> 類型的變量分配Action(Of Base) (在 Visual Basic 中爲 Action<Derived>)的實例。函數

  • Invarianceui

    這意味着,你只能使用原始指定的類型;固定泛型類型參數既不是協變類型,也不是逆變類型。spa

    你沒法向 List<Base> 類型的變量分配 List(Of Base)(在 Visual Basic 中爲 List<Derived>)的實例,反之亦然。code

利用協變類型參數,你能夠執行很是相似於普通的多態性的分配,如如下代碼中所示。

IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d; // 協變

List<T> 類實現 IEnumerable<T> 接口,所以 List<Derived> (在 Visual Basic 中爲List(Of Derived) )實現 IEnumerable<Derived>。 協變類型參數將執行其他的工做。

相反,逆變看起來卻不夠直觀。 下面的示例建立類型 Action<Base> (在 Visual Basic 中爲Action(Of Base) )的委託,而後將此委託分配給類型 Action<Derived>的變量。

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b; // 逆變
d(new Derived());

此示例看起來是倒退了,但它是可編譯和運行的類型安全代碼。 因爲 lambda 表達式與其自身所分配到的委託相匹配,所以它會定義一個方法,此方法採用一個類型 Base 的參數且沒有返回值。 能夠將結果委託分配給類型類型 Action<Derived> 的變量,由於 T 委託的類型參數 Action<T> 是逆變類型參數。 因爲 T 指定了一個參數類型,所以該代碼是類型安全代碼。 在調用類型 Action<Base> 的委託(就像它是類型 Action<Derived>的委託同樣)時,其參數必須屬於類型 Derived。 始終能夠將此實參安全地傳遞給基礎方法,由於該方法的形參屬於類型 Base

一般,協變類型參數可用做委託的返回類型,而逆變類型參數可用做參數類型。 對於接口,協變類型參數可用做接口的方法的返回類型,而逆變類型參數可用做接口的方法的參數類型。

協變和逆變統稱爲「變體」 。 未標記爲協變或逆變的泛型類型參數稱爲「固定參數」 。 有關公共語言運行時中變體的事項的簡短摘要:

  • 在 .NET Framework 4 中,Variant 類型參數僅限於泛型接口和泛型委託類型。

  • 泛型接口或泛型委託類型能夠同時具備協變和逆變類型參數。

  • 變體僅適用於引用類型;若是爲 Variant 類型參數指定值類型,則該類型參數對於生成的構造類型是不變的。

  • 變體不適用於委託組合。 也就是說,在給定類型 Action<Derived> 和 Action<Base>(在 Visual Basic 中爲Action(Of Derived) 和 Action(Of Base) )的兩個委託的狀況下,沒法將第二個委託與第一個委託結合起來,儘管結果將是類型安全的。 變體容許將第二個委託分配給類型 Action<Derived>的變量,但只能在這兩個委託的類型徹底匹配的狀況下對它們進行組合。

具備協變類型參數的泛型接口

從 .NET Framework 4 開始,某些泛型接口具備協變類型參數;例如:IEnumerable<T>IEnumerator<T>IQueryable<T> 和 IGrouping<TKey,TElement>。 因爲這些接口的全部類型參數都是協變類型參數,所以這些類型參數只用於成員的返回類型。

下面的示例闡釋了協變類型參數。 此示例定義了兩個類型: Base 具備一個名爲 PrintBases 的靜態方法,該方法採用 IEnumerable<Base> (在 Visual Basic 中爲IEnumerable(Of Base) )並輸出元素。 Derived 繼承自 Base。 此示例建立一個空 List<Derived> (在 Visual Basic 中爲List(Of Derived) ),而且說明能夠將該類型傳遞給 PrintBases 且在不進行強制轉換的狀況下將該類型分配給類型 IEnumerable<Base> 的變量。 List<T> 實現 IEnumerable<T>,它具備一個協變類型參數。 協變類型參數是可以使用 IEnumerable<Derived> 的實例而非 IEnumerable<Base>的緣由。

using System;
using System.Collections.Generic;

class Base
{
    public static void PrintBases(IEnumerable<Base> bases)
    {
        foreach(Base b in bases)
        {
            Console.WriteLine(b);
        }
    }
}

class Derived : Base
{
    public static void Main()
    {
        List<Derived> dlist = new List<Derived>();

        Derived.PrintBases(dlist);
        IEnumerable<Base> bIEnum = dlist;
    }
}
具備逆變泛型類型參數的泛型接口

從 .NET Framework 4 開始,某些泛型接口具備逆變類型參數;例如:IComparer<T>IComparable<T> 和 IEqualityComparer<T>。 因爲這些接口只具備逆變類型參數,所以這些類型參數只用做接口成員中的參數類型。

下面的示例闡釋了逆變類型參數。 該示例定義具備MustInherit 屬性的抽象(在 Visual Basic 中爲 Shape ) Area 類。 該示例還定義一個實現 ShapeAreaComparer (在 Visual Basic 中爲 IComparer<Shape> )的IComparer(Of Shape) 類。 IComparer<T>.Compare 方法的實現基於 Area 屬性的值,因此 ShapeAreaComparer 可用於按區域對 Shape 對象排序。

Circle 類繼承 Shape 並重寫 Area。 該示例建立 SortedSet<T> 對象的 Circle ,使用採用 IComparer<Circle> (在 Visual Basic 中爲IComparer(Of Circle) )的構造函數。 可是,該對象不傳遞 IComparer<Circle>,而是傳遞一個用於實現 ShapeAreaComparer 的 IComparer<Shape>對象。 當代碼須要派生程度較大的類型的比較器 (Shape) 時,該示例能夠傳遞派生程度較小的類型的比較器 (Circle),由於 IComparer<T> 泛型接口的類型參數是逆變參數。

向 Circle 中添加新 SortedSet<Circle>對象時,每次將新元素與現有元素進行比較時,都會調用 IComparer<Shape>.Compare 對象的IComparer(Of Shape).Compare 方法(在 Visual Basic 中爲 ShapeAreaComparer 方法)。 方法 (Shape) 的參數類型比被傳遞的類型 (Circle) 的派生程度小,因此調用是類型安全的。 逆變使 ShapeAreaComparer 能夠對派生自 Shape的任意單個類型的集合以及混合類型的集合排序。

using System;
using System.Collections.Generic;

abstract class Shape
{
    public virtual double Area { get { return 0; }}
}

class Circle : Shape
{
    private double r;
    public Circle(double radius) { r = radius; }
    public double Radius { get { return r; }}
    public override double Area { get { return Math.PI * r * r; }}
}

class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
    int IComparer<Shape>.Compare(Shape a, Shape b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.Area.CompareTo(b.Area);
    }
}

class Program
{
    static void Main()
    {
        // 能夠傳遞實現IComparer<shape>的shapeareComparer,
// 即便sortedset<circle>的構造函數須要IComparer<circle>,
// 由於IComparer<t>的類型參數T是反向的。
SortedSet<Circle> circlesByArea = new SortedSet<Circle>(new ShapeAreaComparer()) { new Circle(7.2),
new Circle(100),
null,
new Circle(.01)
};
foreach (Circle c in circlesByArea) { Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area); } } } /* 輸出結果: null Circle with area 0.000314159265358979 Circle with area 162.860163162095 Circle with area 31415.9265358979 */
具備 Variant 類型參數的泛型委託
在 .NET Framework 4 中,Func 泛型委託(如 Func<T,TResult>)具備協變返回類型和逆變參數類型。 Action 泛型委託(如 Action<T1,T2>)具備逆變參數類型。 這意味着,能夠將委託指派給具備派生程度較高的參數類型和(對於 Func 泛型委託)派生程度較低的返回類型的變量。

Func 泛型委託的最後一個泛型類型參數指定委託簽名中返回值的類型。 該參數是協變的(out 關鍵字),而其餘泛型類型參數是逆變的(in 關鍵字)。

下面的代碼闡釋這一點。 第一段代碼定義了一個名爲 Base的類、一個名爲 Derived 的類(此類繼承 Base)和另外一個具備名爲 static 的Shared 方法(在 Visual Basic 中爲 MyMethod)的類。 該方法採用 Base 的實例,並返回 Derived 的實例。 (若是參數是 Derived 的實例,則 MyMethod 將返回該實例;若是參數是 Base 的實例,則 MyMethod 將返回 Derived 的新實例。)在 Main() 中,該示例建立一個表示 Func<Base, Derived> 的 Func(Of Base, Derived)(在 Visual Basic 中爲 MyMethod)的實例,並將此實例存儲在變量 f1 中。
 1 public class Base {}
 2 public class Derived : Base {}
 3 
 4 public class Program
 5 {
 6     public static Derived MyMethod(Base b)
 7     {
 8         return b as Derived ?? new Derived();
 9     }
10 
11     static void Main() 
12     {
13         Func<Base, Derived> f1 = MyMethod;
14         
15         /* 能夠將委託分配給類型 Func<Base, Base> (在 Visual Basic 中爲Func(Of Base, Base) )的變量,由於返回類型是協變的。*/
16         // 協變返回類型
17         Func<Base, Base> f2 = f1;
18         Base b2 = f2(new Base());
19         
20         /* 能夠將委託分配給類型 Func<Derived, Derived> (在 Visual Basic 中爲Func(Of Derived, Derived) )的變量,由於參數類型是逆變的。*/
21         // 逆變參數類型
22         Func<Derived, Derived> f3 = f1;
23         Derived d3 = f3(new Derived());
24         
25         /* 能夠將委託分配給類型 Func<Derived, Base> (在 Visual Basic 中爲Func(Of Derived, Base) )的變量,從而將逆變參數類型和協變返回類型的做用結合起來。*/
26         // 協變返回類型和逆變參數類型。
27         Func<Derived, Base> f4 = f1;
28         Base b4 = f4(new Derived());
29     }
30 }
31         

泛型委託和非泛型委託中的變體

在上面的代碼中, MyMethod 的簽名與所構造的泛型委託 Func<Base, Derived> (在 Visual Basic 中爲Func(Of Base, Derived) )的簽名徹底匹配。 此示例說明,只要全部委託類型都是從泛型委託類型 Func<T,TResult>構造的,就能夠將此泛型委託存儲在具備派生程度更大的參數類型和派生程度更小的返回類型的變量或方法參數中。

這一點很是重要。 泛型委託的類型參數中的協方差和逆變的效果相似於普通委託綁定中的協方差和逆變的效果(請參閱委託中的差別 (C#) 和委託中的差別 (Visual Basic))。 可是,委託綁定中的變化適用於全部委託類型,而不只僅適用於具備 Variant 類型參數的泛型委託類型。 此外,經過委託綁定中的變化,能夠將方法綁定到具備限制較多的參數類型和限制較少的返回類型的任何委託,而對於泛型委託的指派,只有在委託類型是基於同一個泛型類型定義構造的時才能夠進行。

下面的示例演示委託綁定中的變化和泛型類型參數中的變化的組合效果。 該示例定義了一個類型層次結構,其中包含三個按派生程度從低到高排列的類型,即Type1的派生程度最低,Type3的派生程度最高。 普通委託綁定中的變化用於將參數類型爲 Type1 、返回類型爲 Type3 的方法綁定到參數類型爲 Type2 、返回類型爲 Type2的泛型委託。 而後,使用泛型類型參數的協變和逆變,將獲得的泛型委託指派給另外一個變量,此變量的泛型委託類型的參數類型爲 Type3 ,返回類型爲 Type1。 第二個指派要求變量類型和委託類型是基於同一個泛型類型定義(在本例中爲 Func<T,TResult>)構造的。

using System;

public class Type1 {}
public class Type2 : Type1 {}
public class Type3 : Type2 {}

public class Program
{
    public static Type3 MyMethod(Type1 t)
    {
        return t as Type3 ?? new Type3();
    }

    static void Main() 
    {
        Func<Type2, Type2> f1 = MyMethod;

        // 協變返回類型和逆變參數類型
        Func<Type3, Type1> f2 = f1;
        Type1 t1 = f2(new Type3());
    }
}
定義 Variant 泛型接口和委託
從 .NET Framework 4 開始,Visual Basic 和 C# 提供了一些關鍵字,利用這些關鍵字,能夠將接口和委託的泛型類型參數標記爲協變或逆變。

從 .NET Framework 2.0 版開始,公共語言運行時支持泛型類型參數上的變化批註。 在 .NET Framework 4 以前,定義包含這些批註的泛型類的惟一方法就是利用 Ilasm.exe(IL 彙編程序) 編譯該類或在動態程序集中發出該類,從而使用 Microsoft 中間語言 (MSIL)。

協變類型參數用 out 關鍵字(在 Visual Basic 中爲Out 關鍵字,在 + MSIL 彙編程序 中爲)標記。 能夠將協變類型參數用做屬於接口的方法的返回值,或用做委託的返回類型。 但不能將協變類型參數用做接口方法的泛型類型約束。

若是接口的方法具備泛型委託類型的參數,則接口類型的協變類型參數可用於指定委託類型的逆變類型參數。

逆變類型參數用 in 關鍵字(在 Visual Basic 中爲In 關鍵字,在 - MSIL 彙編程序 中爲)標記。 能夠將逆變類型參數用做屬於接口的方法的參數類型,或用做委託的參數類型。 也能夠將逆變類型參數用做接口方法的泛型類型約束。

只有接口類型和委託類型才能具備 Variant 類型參數。 接口或委託類型能夠同時具備協變和逆變類型參數。

Visual Basic 和 C# 不容許違反協變和逆變類型參數的使用規則,也不容許將協變和逆變批註添加到接口和委託類型以外的類型參數中。 MSIL 彙編程序 不執行此類檢查,但若是你嘗試加載違反規則的類型,則會引起 TypeLoadException 。

有關信息和示例代碼,請參閱泛型接口中的差別 (C#) 和泛型接口中的差別 (Visual Basic)

Variant 泛型接口和委託類型的列表

在 .NET Framework 4 中,下面的接口和委託類型具備協變和/或逆變類型參數。

 
相關文章
相關標籤/搜索