泛型中的共變數和反變數

Covariant 和 Contravariant 泛型型別在指派和使用泛型型別時提供更大的彈性。 例如,Covariant 型別參數可使您讓指派看起來更像一般多型。 假設您有基底類別和衍生類別,名稱分別為 Base 和 Derived。 多型可讓您將 Derived 的執行個體指派給 Base 型別的變數。 同樣地,因為 IEnumerable<T> 介面的型別參數是 Covariant,您可以將 IEnumerable<Derived> (在 Visual Basic 中為 IEnumerable(Of Derived)) 的執行個體指派給 IEnumerable<Base> 型別的變數,如下列程式碼所示。

Dim d As IEnumerable(Of Derived) = New List(Of Derived)
Dim b As IEnumerable(Of Base) = d
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;

List<T> 類別會實作 IEnumerable<T> 介面,因此 List<Derived> (在 Visual Basic 中則為 List(Of Derived)) 會實作 IEnumerable<Derived>。 Covariant 型別參數會完成其餘工作。

Covariance 看起來非常自然,因為它看起來像是多型。 而 Contravariance 看起來則違反直覺。 下列範例會建立 Action<Base> (在 Visual Basic 中則為 Action(Of Base)) 型別的委派,然後將該委派指派給 Action<Derived> 型別的變數。

Dim b As Action(Of Base) = Sub(target As Base) 
                               Console.WriteLine(target.GetType().Name)
                           End Sub
Dim d As Action(Of Derived) = b
d(New Derived())
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

這看起來是反向的,但是在編譯和執行時是型別安全的程式碼。 Lambda 運算式符合指派的委派,所以它定義的方法會接受一個 Base 型別的參數,並且沒有傳回值。 結果產生的委派可以指派給 Action<Derived> 型別的變數,因為 Action<T> 委派的 T 型別參數是 Contravariant。 程式碼是型別安全的,因為 T 指定參數型別。 當 Action<Base> 型別的委派當成 Action<Derived> 型別的委派被叫用時,引數必須是 Derived 型別。 此引數永遠可以安全地傳遞至基礎方法,因為方法的參數是 Base 型別。

一般來說,Covariant 型別參數可以用來做為委派的傳回型別,而 Contravariant 型別參數可以用來做為參數型別。 例如,Covariant 型別參數可以用來做為介面方法的傳回型別,而 Contravariant 型別參數可以用來做為介面方法的參數型別。

共變性和逆變性合稱為「變異性」(Variance)。 未標示 Covariant 或 Contravariant 的泛型型別參數,稱為 Invariant 參數。 Common Language Runtime 中變異數事實的簡短摘要。

  • 在 .NET Framework 4 版 中,Variant 型別參數僅限為泛型介面和泛型委派型別。

  • 泛型介面或泛型委派型別可以同時具有 Covariant 和 Contravariant 型別參數。

  • 變異數只適用於參考型別,因此如果將 Variant 型別參數指定為實值型別,該型別參數最後建構的型別會是 Invariant。

  • 變異數不適用於委派組合。 也就是說,如果有分別適用於 Action<Derived> 和 Action<Base> (在 Visual Basic 中則為 Action(Of Derived) 和 Action(Of Base)) 型別的兩個委派,您無法將第二個委派與第一個委派組合 (雖然結果會是型別安全的)。 變異數允許將第二個委派指派給 Action<Derived> 型別的變數,但是委派只有在型別完全相符時才能組合。

下列小節將詳細說明 Covariant 和 Contravariant 型別參數:

  • 具有 Covariant 型別參數的泛型介面

  • 具有 Contravariant 泛型型別參數的泛型介面

  • 具有 Variant 型別參數的泛型委派

  • 定義 Variant 泛型介面與委派

  • Variant 泛型介面與委派型別的清單

具有 Covariant 型別參數的泛型介面

從 .NET Framework 4 開始,數個泛型介面便具有 Covariant 型別參數,例如:IEnumerable<T>IEnumerator<T>IQueryable<T>IGrouping<TKey, TElement>。這些介面的所有型別參數都是 Covariant,因此型別參數只能用於其成員的傳回型別。 

下列範例會說明 Covariant 型別參數。 這個範例定義兩個型別: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>,後者具有單一 Covariant 型別參數。 Covariant 型別參數是可以使用 IEnumerable<Derived> 的執行個體而不能使用 IEnumerable<Base> 的執行個體的原因。

Imports System.Collections.Generic

Class Base
    Public Shared Sub PrintBases(ByVal bases As IEnumerable(Of Base))
        For Each b As Base In bases
            Console.WriteLine(b)
        Next
    End Sub    
End Class

Class Derived
    Inherits Base

    Shared Sub Main()
        Dim dlist As New List(Of Derived)()

        Derived.PrintBases(dlist)
        Dim bIEnum As IEnumerable(Of Base) = dlist
    End Sub
End Class
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;
    }
}

回到頁首

具有 Contravariant 泛型型別參數的泛型介面

從 .NET Framework 4 開始,數個泛型介面便具有 Contravariant 型別參數,例如:IComparer<T>IComparable<T>IEqualityComparer<T>。 這些介面只有 Contravariant 型別參數,因此型別參數只用來做為介面成員中的參數型別。

下列範例會說明 Contravariant 型別參數。 範例定義了包含 Area 屬性的抽象 (在 Visual Basic 中為 MustInherit) Shape 類別。 範例還定義了實作 IComparer<Shape> (在 Visual Basic 中則為 IComparer(Of Shape)) 的 ShapeAreaComparer 類別。 IComparer<T>.Compare 方法的實作是以 Area 屬性值為基礎,因此可以使用 ShapeAreaComparer 依區域排序 Shape 物件。

Circle 類別會繼承 Shape 並覆寫 Area。 此範例會使用採用 IComparer<Circle> (在 Visual Basic 中為 IComparer(Of Circle)) 的建構函式建立 Circle 物件的 SortedSet<T>。 不過,此範例不會傳遞 IComparer<Circle>,而是傳遞實作 IComparer<Shape> 的 ShapeAreaComparer 物件。 當程式碼呼叫衍生程度較大型別 (Circle) 的比較子時,範例可以傳遞衍生程度較小型別 (Shape) 的比較子,因為 IComparer<T> 泛型介面的型別參數為 Contravariant。

將新的 Circle 物件加入至 SortedSet<Circle> 時,ShapeAreaComparer 物件的 IComparer<Shape>.Compare 方法 (在 Visual Basic 中為 IComparer(Of Shape).Compare 方法) 會在每次新項目與現有項目比較時呼叫。 這個方法的型別參數 (Shape) 相較於傳遞的型別 (Circle),其衍生程度較小,因此該呼叫具備型別安全。 反變數 (Contravariance) 可讓 ShapeAreaComparer 排序衍生自 Shape 的任何單一型別集合,以及混合型別集合。

Imports System.Collections.Generic

MustInherit Class Shape
    Public MustOverride ReadOnly Property Area As Double
End Class

Class Circle
    Inherits Shape

    Private r As Double 
    Public Sub New(ByVal radius As Double)
        r = radius
    End Sub 
    Public ReadOnly Property Radius As Double
        Get
            Return r
        End Get
    End Property
    Public Overrides ReadOnly Property Area As Double
        Get
            Return Math.Pi * r * r
        End Get
    End Property
End Class

Class ShapeAreaComparer
    Implements System.Collections.Generic.IComparer(Of Shape)

    Private Function AreaComparer(ByVal a As Shape, ByVal b As Shape) As Integer _
            Implements System.Collections.Generic.IComparer(Of Shape).Compare
        If a Is Nothing Then Return If(b Is Nothing, 0, -1)
        Return If(b Is Nothing, 1, a.Area.CompareTo(b.Area))
    End Function
End Class

Class Program
    Shared Sub Main()
        ' You can pass ShapeAreaComparer, which implements IComparer(Of Shape),
        ' even though the constructor for SortedSet(Of Circle) expects 
        ' IComparer(Of Circle), because type parameter T of IComparer(Of T)
        ' is contravariant.
        Dim circlesByArea As New SortedSet(Of Circle)(New ShapeAreaComparer()) _
            From { New Circle(7.2), New Circle(100), Nothing, New Circle(.01) }

        For Each c As Circle In circlesByArea
            Console.WriteLine(If(c Is Nothing, "Nothing", "Circle with area " & c.Area))
        Next
    End Sub
End Class

' This code example produces the following output:
'
'Nothing
'Circle with area 0.000314159265358979
'Circle with area 162.860163162095
'Circle with area 31415.9265358979
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()
    {
        // You can pass ShapeAreaComparer, which implements IComparer<Shape>,
        // even though the constructor for SortedSet<Circle> expects 
        // IComparer<Circle>, because type parameter T of IComparer<T> is
        // contravariant.
        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);
        }
    }
}

/* This code example produces the following output:

null
Circle with area 0.000314159265358979
Circle with area 162.860163162095
Circle with area 31415.9265358979
 */

回到頁首

具有 Variant 型別參數的泛型委派

在 .NET Framework 4 中,Func 泛型委派 (例如 Func<T, TResult>) 具有 Covariant 傳回型別和 Contravariant 型別參數, Action 泛型委派 (例如 Action<T1, T2>) 則具有 Contravariant 型別參數。 這表示可以將委派指派給具有衍生程度較大參數型別及 (如果是 Func 泛型委派) 衍生程度較小的傳回型別的變數。

注意事項注意事項

Func 泛型委派的最後一個泛型型別參數會指定委派簽章中的傳回值型別。這個參數是 Covariant (out 關鍵字),而其他泛型型別參數則是 Contravariant (in 關鍵字)。

以下的程式碼可說明這點。 程式碼的第一段會定義名為 Base 的類別、繼承 Base 的 Derived 類別,和另一個具有 static 方法 (在 Visual Basic 中則為 Shared 方法) 的 MyMethod 類別。 這個方法會接受 Base 的執行個體,然後傳回 Derived 的執行個體 (如果引數為 Derived 的執行個體,MyMethod 即傳回此執行個體;如果引數為 Base 的執行個體,MyMethod 則傳回新的 Derived 執行個體)。在 Main() 中,這個範例會建立代表 MyMethod 的 Func<Base, Derived> (在 Visual Basic 中則為 Func(Of Base, Derived)) 執行個體,並將它儲存在變數 f1 中。

Public Class Base 
End Class
Public Class Derived
    Inherits Base
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal b As Base) As Derived 
        Return If(TypeOf b Is Derived, b, New Derived())
    End Function

    Shared Sub Main() 
        Dim f1 As Func(Of Base, Derived) = AddressOf MyMethod
public class Base {}
public class Derived : Base {}

public class Program
{
    public static Derived MyMethod(Base b)
    {
        return b as Derived ?? new Derived();
    }

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

程式碼的第二段顯示可以將委派指派給型別為 Func<Base, Base> (在 Visual Basic 中則為 Func(Of Base, Base)) 的變數,因為傳回型別是 Covariant。

' Covariant return type.
Dim f2 As Func(Of Base, Base) = f1
Dim b2 As Base = f2(New Base())
// Covariant return type.
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base());

程式碼的第三段顯示可以將委派指派給型別為 Func<Derived, Derived> (在 Visual Basic 中則為 Func(Of Derived, Derived)) 的變數,因為參數型別是 Contravariant。

' Contravariant parameter type.
Dim f3 As Func(Of Derived, Derived) = f1
Dim d3 As Derived = f3(New Derived())
// Contravariant parameter type.
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());

程式碼的最後一段顯示結合 Contravariant 參數型別與 Covariant 傳回型別的效果,即可將委派指派給型別為 Func<Derived, Base> (在 Visual Basic 中則為 Func(Of Derived, Base)) 的變數。

' Covariant return type and contravariant parameter type.
Dim f4 As Func(Of Derived, Base) = f1
Dim b4 As Base = f4(New Derived())
// Covariant return type and contravariant parameter type.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());

泛型和非泛型委派中的變異數

在上述的程式碼中,MyMethod 的簽章與建構的泛型委派 Func<Base, Derived> (在 Visual Basic 中則為 Func(Of Base, Derived)) 之簽章完全相符。 這個範例顯示,只要所有委派型別都是從泛型委派型別 Func<T, TResult> 建構的,即可將這個泛型委派儲存在具有衍生程度較大參數型別及衍生程度較小傳回型別的變數或方法參數中。

這一點很重要。 泛型委派參數型別中的 Covariance 及 Contravariance 效果類似於一般委派繫結中的 Covariance 及 Contravariance 效果 (請參閱委派中的變異數 (C# 和 Visual Basic))。 不過,委派繫結中的變異數可以使用所有的委派型別,而不只是具有 Variant 型別參數的泛型委派型別。 此外,委派繫結中的變異數可讓方法繫結至任何具有較嚴格參數型別及較不嚴格傳回型別的委派,而泛型委派的指派只適用於這兩種委派型別都是從相同泛型型別定義建構的情況。

在下列範例中,會說明委派繫結中的變異數與泛型型別參數中的變異數合併之效果。 範例中定義包含三個型別的型別階層,其中衍生程度最小的是 Type1,而最大的是 Type3。 在一般委派繫結中,會使用變異數以將參數型別為 Type1 且傳回型別為 Type3 的方法繫結至參數型別為 Type2 且傳回型別為 Type2 的泛型委派。 然後,範例會使用泛型型別參數的共變數和 Contravariance,將產生的泛型委派指派給另一個參數型別為 Type3 且傳回型別為 Type1 的泛型委派型別變數。 在第二項指派中,變數型別和委派型別都必須是從相同的泛型型別定義 (在本範例中為 Func<T, TResult>) 建構的。

Public Class Type1 
End Class
Public Class Type2
    Inherits Type1
End Class
Public Class Type3
    Inherits Type2
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal t As Type1) As Type3
        Return If(TypeOf t Is Type3, t, New Type3())
    End Function

    Shared Sub Main() 
        Dim f1 As Func(Of Type2, Type2) = AddressOf MyMethod

        ' Covariant return type and contravariant parameter type.
        Dim f2 As Func(Of Type3, Type1) = f1
        Dim t1 As Type1 = f2(New Type3())
    End Sub
End Class
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;

        // Covariant return type and contravariant parameter type.
        Func<Type3, Type1> f2 = f1;
        Type1 t1 = f2(new Type3());
    }
}

回到頁首

定義 Variant 泛型介面與委派

從 .NET Framework 4 開始,Visual Basic 和 C# 提供可讓您將介面及委派的泛型型別參數標記為 Covariant 或 Contravariant 的關鍵字。

注意事項注意事項

從 .NET Framework 2.0 版開始,Common Language Runtime 在泛型型別參數上支援變異數附註。在 .NET Framework 4 以前,若要定義具有這些附註的泛型類別,唯一的辦法就是使用 Microsoft Intermediate Language (MSIL),即透過 Ilasm.exe (MSIL 組譯工具) 編譯類別,或是在動態組件中發出類別。

Covariant 型別參數是以 out 關鍵字 (在 Visual Basic 中為 Out 關鍵字,在 MSIL 組合語言中則為 +) 來標記。 您可以使用 Covariant 型別參數當做屬於介面之方法的傳回值,或當做委派的傳回型別。 但是,您不能將 Covariant 型別參數當做介面方法的泛型型別條件約束使用。

注意事項注意事項

如果介面的方法具有泛型委派型別的參數,就可以使用介面型別的 Covariant 型別參數指定委派型別的 Contravariant 型別參數。

Contravariant 型別參數是以 in 關鍵字 (在 Visual Basic 中為 In 關鍵字,而在 MSIL 組合語言中則為 -) 來標記。 您可以使用 Contravariant 型別參數當做屬於介面之方法的參數型別,或當做委派的參數型別。 此外,您也可以將 Contravariant 型別參數當做介面方法的泛型型別條件約束使用。

只有介面型別和委派型別可以有 Variant 型別參數。 介面或委派型別可以同時具有 Covariant 和 Contravariant 型別參數。

Visual Basic 和 C# 不允許您違反使用 Covariant 和 Contravariant 型別參數的規則,也不允許您將 Covariant 和 Contravariant 附註加入至介面及委派以外型別的型別參數。 MSIL 組合語言不會執行這類檢查,如果您嘗試載入違反規則的型別,便會擲回 TypeLoadException

如需詳細資訊與範例程式碼,請參閱泛型介面中的變異數 (C# 和 Visual Basic)

回到頁首

Variant 泛型介面與委派型別的清單

在 .NET Framework 4 中,下列介面和委派型別具有 Covariant 及/或 Contravariant 型別參數。 

型別

Covariant 型別參數

Contravariant 型別參數

Action<T>Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>

Comparison<T>

Converter<TInput, TOutput>

Func<TResult>

Func<T, TResult>Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult>

IComparable<T>

Predicate<T>

IComparer<T>

IEnumerable<T>

IEnumerator<T>

IEqualityComparer<T>

IGrouping<TKey, TElement>

IOrderedEnumerable<TElement>

IOrderedQueryable<T>

IQueryable<T>

回到頁首

請參閱

概念

委派中的變異數 (C# 和 Visual Basic)

其他資源

共變數和反變數 (C# 和 Visual Basic)