Kowariancja i kontrawariancja w rodzajach ogólnych

Kowariancja i kontrawariancja to terminy odwołujące się do możliwości używania bardziej pochodnego typu (bardziej szczegółowego) lub mniejszego typu pochodnego (mniej specyficznego) niż pierwotnie określone. Parametry typu ogólnego obsługują kowariancję i kontrawariancję, aby umożliwić większą elastyczność przypisywania i używania typów ogólnych.

W przypadku odwoływania się do systemu typów, kowariancji, kontrawariancji i wariancji mają następujące definicje. W przykładach przyjęto założenie, że klasa bazowa o nazwie Base i klasa pochodna o nazwie Derived.

  • Covariance

    Umożliwia użycie bardziej pochodnego typu niż pierwotnie określony.

    Wystąpienie klasy można przypisać IEnumerable<Derived> do zmiennej typu IEnumerable<Base>.

  • Contravariance

    Umożliwia użycie bardziej ogólnego (mniej pochodnego) typu niż oryginalnie określony.

    Wystąpienie klasy można przypisać Action<Base> do zmiennej typu Action<Derived>.

  • Invariance

    Oznacza, że można użyć tylko określonego typu. Niezmienny parametr typu ogólnego nie jest ani kowariantny, ani kontrawariantny.

    Nie można przypisać wystąpienia do zmiennej List<Base> typu List<Derived> lub odwrotnie.

Kowariantne parametry typu umożliwiają tworzenie przypisań, które wyglądają podobnie jak zwykły polimorfizm, jak pokazano w poniższym kodzie.

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

Klasa List<T> implementuje IEnumerable<T> interfejs, więc List<Derived> (List(Of Derived) w Visual Basic) implementuje IEnumerable<Derived>element . Kowariantny parametr typu wykonuje resztę zadania.

Z drugiej strony, kontrawariancja wydaje się nielogiczna. Poniższy przykład tworzy delegata typu Action<Base> (Action(Of Base) w Visual Basic), a następnie przypisuje ten delegat do zmiennej typu Action<Derived>.

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new 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())

Wydaje się to wsteczne, ale jest to bezpieczny dla typów kod, który można skompilować i uruchomić. Wyrażenie lambda pasuje do delegata, do którego jest przypisany, dlatego definiuje metodę, która przyjmuje jeden parametr typu Base i nie ma zwracanej wartości. Wynikowy delegat można przypisać do zmiennej typu Action<Derived> , ponieważ parametr T typu delegata Action<T> jest kontrawariantny. Kod jest bezpieczny dla typu, ponieważ T określa typ parametru. Gdy delegat typu Action<Base> jest wywoływany tak, jakby był pełnomocnikiem typu Action<Derived>, jego argument musi być typu Derived. Ten argument zawsze można bezpiecznie przekazać do bazowej metody, ponieważ parametr metody jest typu Base.

Ogólnie, kowariantnego parametru typu można użyć jako typu zwracanego delegata, a kontrawariantnych parametrów typu można używać jako typów parametrów. Na przykład kowariantnych parametrów typu można używać jako typów zwracanych metod interfejsu, a kontrawariantnych parametrów typu można używać jako typów parametrów metod interfejsu.

Kowariancja i kontrawariancja są łącznie określane jako wariancja. Parametr typu ogólnego, który nie jest oznaczony kowariantnym lub kontrawariantem, jest określany jako niezmienny. Krótkie podsumowanie faktów na temat wariancji w środowisku uruchomieniowym języka wspólnego:

  • Parametry typu wariantu są ograniczone do interfejsu ogólnego i typów delegatów ogólnych.

  • Ogólny typ interfejsu lub delegata może mieć kowariantne i kontrawariantne parametry typu.

  • Wariancja dotyczy tylko typów referencyjnych; określenie typu wartości dla wariantnego parametru typu spowoduje, że parametr typu będzie inwariantny dla wynikowego skonstruowanego typu.

  • Wariancja nie dotyczy kombinacji delegatów. Oznacza to, że biorąc pod uwagę dwa delegaty typów Action<Derived> i (Action(Of Derived) i Action<Base>Action(Of Base) w Visual Basic), nie można połączyć drugiego delegata z pierwszym, chociaż wynik będzie bezpieczny. Wariancja umożliwia przypisanie drugiego delegata do zmiennej typu Action<Derived>, ale delegaci mogą łączyć się tylko wtedy, gdy ich typy są dokładnie zgodne.

  • Począwszy od języka C# 9, kowariantne typy zwracane są obsługiwane. Metoda zastępowania może zadeklarować bardziej pochodny typ zwracany przez metodę, która zastępuje, a zastępowanie właściwości tylko do odczytu może zadeklarować bardziej pochodny typ.

Interfejsy ogólne z kowariantnymi parametrami typu

Kilka interfejsów ogólnych ma kowariantne parametry typu, na przykład IEnumerable<T>, IEnumerator<T>, IQueryable<T>i IGrouping<TKey,TElement>. Wszystkie parametry typu tych interfejsów są kowariantne, więc parametry typu są używane tylko dla typów zwracanych elementów członkowskich.

W poniższym przykładzie pokazano kowariantne parametry typu. W przykładzie zdefiniowano dwa typy: Base ma statyczną metodę o nazwie PrintBases , która przyjmuje IEnumerable<Base> element (IEnumerable(Of Base) w Visual Basic) i wyświetla elementy. Derived dziedziczy z Base. W przykładzie jest tworzony pusty List<Derived> (List(Of Derived) w Visual Basic) i pokazano, że ten typ można przekazać do PrintBases zmiennej typu IEnumerable<Base> i przypisać do niej bez rzutowania. List<T> implementuje IEnumerable<T>element , który ma jeden kowariantny parametr typu. Kowariantny parametr typu jest powodem, dla którego można użyć wystąpienia IEnumerable<Derived> zamiast 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;
    }
}
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

Interfejsy ogólne z kontrawariantnymi parametrami typu

Kilka interfejsów ogólnych ma kontrawariantne parametry typu; na przykład: IComparer<T>, IComparable<T>i IEqualityComparer<T>. Te interfejsy mają tylko kontrawariantne parametry typu, więc te parametry typów są używane tyko jako typy parametrów w elementach członkowskich tych interfejsów.

W poniższym przykładzie pokazano kontrawariantne parametry typu. W przykładzie zdefiniowano abstrakcyjną (MustInherit w Visual Basic) Shape klasę z właściwością Area . W przykładzie zdefiniowano również klasę ShapeAreaComparer , która implementuje IComparer<Shape> (IComparer(Of Shape) w Visual Basic). Implementacja IComparer<T>.Compare metody jest oparta na wartości Area właściwości, więc ShapeAreaComparer może służyć do sortowania Shape obiektów według obszaru.

Klasa Circle dziedziczy Shape i zastępuje Areaelement . W przykładzie tworzony jest CircleSortedSet<T> obiekt przy użyciu konstruktora, który przyjmuje IComparer<Circle> element (IComparer(Of Circle) w Visual Basic). Jednak zamiast przekazywać element IComparer<Circle>, przykład przekazuje ShapeAreaComparer obiekt, który implementuje IComparer<Shape>element . Przykład może przekazać porównanie mniej pochodnego typu (Shape), gdy kod wywołuje porównanie bardziej pochodnego typu (Circle), ponieważ parametr IComparer<T> typu interfejsu ogólnego jest kontrawariantny.

Po dodaniu nowego Circle obiektu do SortedSet<Circle>IComparer<Shape>.Compare metody (IComparer(Of Shape).Compare metoda w Visual Basic) ShapeAreaComparer obiektu jest wywoływana za każdym razem, gdy nowy element jest porównywany z istniejącym elementem. Typ parametru metody (Shape) jest mniejszy niż typ przekazywany (Circle), więc wywołanie jest bezpieczne. Kontrawariancja umożliwia sortowanie ShapeAreaComparer kolekcji dowolnego typu, a także mieszanej kolekcji typów, które pochodzą z Shapeklasy .

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
 */
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

Delegaty ogólne z parametrami typu wariantu

Delegaty Func ogólne, takie jak Func<T,TResult>, mają kowariantne typy zwracane i kontrawariantne typy parametrów. Delegaty Action ogólne, takie jak Action<T1,T2>, mają kontrawariantne typy parametrów. Oznacza to, że delegaci mogą być przypisywani do zmiennych, które mają bardziej pochodne typy parametrów i (w przypadku Func delegatów ogólnych) mniej pochodnych typów zwracanych.

Uwaga

Ostatni ogólny parametr Func typu delegatów ogólnych określa typ zwracanej wartości w podpisie delegata. Jest kowariantny (out słowo kluczowe), natomiast inne parametry typu ogólnego są kontrawariantne (in słowo kluczowe).

Ilustruje to poniższy kod. Pierwszy fragment kodu definiuje klasę o nazwie Base, klasę o nazwie , która dziedziczy Base, i inną klasę static za pomocą metody (Shared w Visual Basic) o nazwie DerivedMyMethod. Metoda przyjmuje wystąpienie Base klasy i zwraca wystąpienie klasy Derived. (Jeśli argument jest wystąpieniem Derivedklasy , MyMethod zwraca go; jeśli argument jest wystąpieniem Baseklasy , MyMethod zwraca nowe wystąpienie Derivedklasy . W Main()pliku przykład tworzy wystąpienie Func<Base, Derived> klasy (Func(Of Base, Derived) w Visual Basic), które reprezentuje MyMethodelement , i przechowuje je w zmiennej f1.

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;
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

Drugi fragment kodu pokazuje, że delegat może być przypisany do zmiennej typu Func<Base, Base> (Func(Of Base, Base) w Visual Basic), ponieważ zwracany typ jest kowariantny.

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

Trzeci fragment kodu pokazuje, że delegat może być przypisany do zmiennej typu Func<Derived, Derived> (Func(Of Derived, Derived) w Visual Basic), ponieważ typ parametru jest kontrawariantny.

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

Ostatni fragment kodu pokazuje, że delegat można przypisać do zmiennej typu Func<Derived, Base> (Func(Of Derived, Base) w Visual Basic), łącząc efekty typu kontrawariantnego typu parametru i kowariantny typ zwracany.

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

Wariancja w delegatach innych niż ogólne

W poprzednim kodzie podpis dokładnie odpowiada podpisowi MyMethod skonstruowanego delegata ogólnego: Func<Base, Derived> (Func(Of Base, Derived) w Visual Basic). W przykładzie pokazano, że ten delegat ogólny może być przechowywany w zmiennych lub parametrach metody, które mają więcej typów parametrów pochodnych i mniej pochodnych typów zwracanych, o ile wszystkie typy delegatów są konstruowane z ogólnego typu Func<T,TResult>delegata .

Jest to ważny punkt. Skutki kowariancji i kontrawariancji w parametrach typu delegatów ogólnych są podobne do skutków kowariancji i kontrawariancji w zwykłym powiązaniu delegata (zobacz Wariancja w delegatach (C#) i wariancja w delegatach (Visual Basic)). Jednak wariancja w powiązaniach delegatów działa ze wszystkimi typami delegatów, a nie tylko z ogólnymi typami delegatów, które mają wariantne parametry typu. Co więcej wariancja w powiązaniach delegatów umożliwia powiązanie metody z dowolnym delegatem, który ma bardziej restrykcyjne typy parametrów i mniej restrykcyjny typ zwracany, podczas gdy przypisanie delegatów ogólnych działa tylko wtedy, gdy oba typy delegatów są konstruowane na podstawie jednej definicji typu ogólnego.

W poniższym przykładzie pokazano połączone efekty zastosowania wariancji w powiązaniu delegatów oraz zastosowania wariancji w parametrach typu ogólnego. W przykładzie zdefiniowano hierarchię typów obejmującą trzy typy, od najmniej pochodnych (Type1) do najbardziej pochodnych (Type3). Wariancja w zwykłym powiązaniu delegata służy do powiązania metody z typem parametru Type1 i zwracanym typem Type3 do delegata ogólnego z typem parametru Type2 i zwracanym typem Type2. Wynikowy delegat ogólny jest następnie przypisywany do innej zmiennej, której typ delegata ogólnego ma parametr typu Type3 i zwracany typ Type1, przy użyciu kowariancji i kontrawariancji parametrów typu ogólnego. Drugie przypisanie wymaga utworzenia zarówno typu zmiennej, jak i typu delegata z tej samej definicji typu ogólnego, w tym przypadku 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;

        // Covariant return type and contravariant parameter type.
        Func<Type3, Type1> f2 = f1;
        Type1 t1 = f2(new Type3());
    }
}
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

Definiowanie wariantów interfejsów ogólnych i delegatów

Język Visual Basic i C# mają słowa kluczowe, które umożliwiają oznaczanie ogólnych parametrów typu interfejsów i delegatów jako kowariantne lub kontrawariantne.

Kowariantny parametr typu jest oznaczony out słowem kluczowym (Out słowo kluczowe w Visual Basic). Kowariantnego parametru typu można użyć jako wartości zwracanej metody, która należy do interfejsu, lub typu zwracanego delegata. Kowariantnego parametru typu nie można użyć jako ograniczenia typu ogólnego dla metod interfejsu.

Uwaga

Jeśli metoda interfejsu ma parametr, który jest typem ogólnym delegatów, kowariantny parametr typu dla typu interfejsu może być używany w celu określenia kontrawariantnego parametru typu dla typu delegata.

Kontrawariantny parametr typu jest oznaczony in słowem kluczowym (In słowo kluczowe w Visual Basic). Kontrawariantnego parametru typu można użyć jako typu parametru metody, która należy do interfejsu, lub typu parametru delegata. Kontrawariantnego parametru typu można użyć jako ograniczenia typu ogólnego dla metody interfejsu.

Tylko typy interfejsów i typy delegatów mogą mieć wariantne parametry typu. Typ interfejsu lub delegata może mieć kowariantne i kontrawariantne parametry typu.

Programy Visual Basic i C# nie zezwalają na naruszanie reguł używania kowariantnych i kontrawariantnych parametrów typu oraz dodawanie adnotacji o kowariancji i kontrawariancji do parametrów typu dla typów innych niż interfejsy i delegaty.

Aby uzyskać informacje i przykładowy kod, zobacz Variance in Generic Interfaces (C#) and Variance in Generic Interfaces (Visual Basic) (Wariancja w interfejsach ogólnych (Visual Basic).

Lista typów

Następujące typy interfejsów i delegatów mają kowariantne i/lub kontrawariantne parametry typu.

Typ Kowariantne parametry typu Kontrawariantne parametry typu
Action<T> do Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16> Tak
Comparison<T> Tak
Converter<TInput,TOutput> Tak Tak
Func<TResult> Tak
Func<T,TResult> do Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult> Tak Tak
IComparable<T> Tak
Predicate<T> Tak
IComparer<T> Tak
IEnumerable<T> Tak
IEnumerator<T> Tak
IEqualityComparer<T> Tak
IGrouping<TKey,TElement> Tak
IOrderedEnumerable<TElement> Tak
IOrderedQueryable<T> Tak
IQueryable<T> Tak

Zobacz też