Udostępnij za pośrednictwem


Kowariancja i kontrawariancja w generykach

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 zapewnić większą elastyczność przypisywania i używania typów ogólnych.

Podczas odwoływania się do systemu typów, podane są następujące definicje dla kowariancji, kontrawariancji i wariancji. Założono, że w przykładach występuje klasa bazowa o nazwie Base oraz klasa pochodna o nazwie Derived.

  • Covariance

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

    Możesz przypisać instancję IEnumerable<Derived> do zmiennej typu IEnumerable<Base>.

  • Contravariance

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

    Możesz przypisać instancję Action<Base> do zmiennej typu Action<Derived>.

  • Invariance

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

    Nie można przypisać wystąpienia typu List<Base> do zmiennej 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 interfejs IEnumerable<T>, więc List<Derived> (List(Of Derived) w Visual Basic) implementuje IEnumerable<Derived>. Kowariantny parametr typu wykonuje resztę.

Kontrawariancja, z drugiej strony, wydaje się nieintuicyjna. 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 nielogiczne, ale jest to bezpieczny typowo kod, który się kompiluje i uruchamia. 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 rzecz biorąc, kowariantny parametr typu może służyć jako zwracany typ delegata, a kontrawariantne parametry typu mogą być używane jako typy parametrów. W przypadku interfejsu kowariantne parametry typu mogą służyć jako typy zwracane metod interfejsu, a kontrawariantne parametry typu mogą być używane jako typy 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 dotyczących zmienności w środowisku uruchomieniowym języka wspólnego (CLR):

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

  • Ogólny interfejs lub ogólny typ delegata może mieć zarówno kowariantne, jak i kontrawariantne parametry typu.

  • Wariancja ma zastosowanie tylko do typów referencyjnych; Jeśli określisz typ wartości dla parametru typu wariantu, ten parametr typu jest niezmienny dla wynikowego skonstruowanego typu.

  • Wariancja nie ma zastosowania do kombinacji delegatów. Oznacza to, że biorąc pod uwagę dwa delegaty typów Action<Derived> i Action<Base> (Action(Of Derived) i Action(Of Base) w Visual Basic), nie można połączyć drugiego delegata z pierwszym, chociaż wynik byłby typowo 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 zwracanych typów elementów członkowskich.

W poniższym przykładzie przedstawiono kowariantne parametry typu. W przykładzie zdefiniowano dwa typy: Base ma statyczną metodę o nazwie PrintBases, która przyjmuje IEnumerable<Base> (IEnumerable(Of Base) w Visual Basic) i wyświetla jego 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 oraz przypisać do zmiennej typu IEnumerable<Base> bez rzutowania. pl-PL: List<T> implementuje IEnumerable<T>, 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 parametry typu są używane tylko jako typy parametrów w elementach członkowskich interfejsów.

W poniższym przykładzie przedstawiono 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 Area. W przykładzie tworzony jest SortedSet<T> z Circle obiektów przy użyciu konstruktora, który przyjmuje IComparer<Circle> (IComparer(Of Circle) w Visual Basic). Jednak zamiast przekazywać IComparer<Circle>, przykład przekazuje obiekt ShapeAreaComparer, który implementuje IComparer<Shape>. Przykład może przekazać obiekt porównujący mniej zaawansowanego typu (Shape), gdy kod wymaga obiektu porównującego bardziej zaawansowanego typu (Circle), ponieważ parametr typu interfejsu generycznego IComparer<T> jest kontrawariantny.

Po dodaniu nowego Circle obiektu do SortedSet<Circle>, metoda IComparer<Shape>.Compare (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 ShapeAreaComparer sortowanie zarówno kolekcji jednego typu, jak i mieszanej kolekcji typów, które wywodzą się z klasy 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()
    {
        // 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 ze zmiennymi parametrami typu

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

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).

Poniższy kod ilustruje to. Pierwszy fragment kodu definiuje klasę o nazwie Base, klasę o nazwie Derived, która dziedziczy Base, oraz inną klasę z metodą static (Shared w Visual Basic) o nazwie MyMethod. Metoda przyjmuje wystąpienie Base i zwraca wystąpienie Derived. (Jeśli argument jest wystąpieniem Derived, MyMethod zwraca go; jeśli argument jest wystąpieniem Base, MyMethod zwraca nowe wystąpienie Derived.) W Main() przykład tworzy wystąpienie Func<Base, Derived> (Func(Of Base, Derived) w Visual Basic), które reprezentuje MyMethod 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 delegata można przypisać do zmiennej typu Func<Derived, Base> (Func(Of Derived, Base) w Visual Basic), łącząc efekty typu kontrawariantnego parametru i typu kowariantnego zwracanej wartości.

// 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 sygnatura MyMethod dokładnie pasuje do sygnatury 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 typów delegatów ogólnych są podobne do efektów kowariancji i kontrawariancji w zwykłym powiązaniu delegatów (zobacz Wariancja w Delegatach (C#) i Wariancja w Delegatach (Visual Basic)). Jednak wariancja w powiązaniu delegata działa ze wszystkimi typami delegatów, a nie tylko z ogólnymi typami delegatów, które mają parametry typu wariantu. Ponadto zmienność w powiązaniu delegata umożliwia przypisanie metody do dowolnego delegata, który ma bardziej restrykcyjne typy parametrów i mniej restrykcyjny typ zwracany. Przypisanie delegatów ogólnych funkcjonuje jednak tylko wtedy, gdy oba typy delegatów zostały zbudowane na podstawie tej samej definicji typu ogólnego.

W poniższym przykładzie pokazano połączone efekty wariancji wiązań delegatów i wariancji parametrów typów ogólnych. 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. Docelowego delegata ogólnego przypisuje się następnie do innej zmiennej, której typ delegata ma parametr typu Type3 i zwraca typ Type1, wykorzystując kowariancję i kontrawariancję 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). Można użyć kowariantnego parametru typu jako wartości zwracanej metody należącej do interfejsu lub jako zwracanego typu delegata. Nie można użyć kowariantnego parametru typu jako ograniczenia typu ogólnego dla metod interfejsu.

Uwaga / Notatka

Jeśli metoda interfejsu ma parametr, który jest ogólnym typem delegata, kowariantny parametr typu interfejsu może służyć do określenia kontrawariantnego parametru typu delegata.

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

Tylko typy interfejsów i typy delegatów mogą mieć parametry typu wariantu. Typ interfejsu lub delegata może mieć zarówno kowariantne, jak i kontrawariantne parametry typu.

Visual Basic i C# nie zezwalają na naruszenie reguł dotyczących używania kowariantnych i kontrawariantnych parametrów typu lub dodawania adnotacji kowariancji i kontrawariancji do parametrów typu innych niż interfejsy i delegaty.

Aby uzyskać informacje i przykładowy kod, zobacz Wariancja w interfejsach ogólnych (C#) oraz 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 także