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 typuIEnumerable<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 typuAction<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>
typuList<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)
iAction<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 typuAction<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 Area
element . W przykładzie tworzony jest Circle
SortedSet<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 Shape
klasy .
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 Derived
MyMethod
. Metoda przyjmuje wystąpienie Base
klasy i zwraca wystąpienie klasy Derived
. (Jeśli argument jest wystąpieniem Derived
klasy , MyMethod
zwraca go; jeśli argument jest wystąpieniem Base
klasy , MyMethod
zwraca nowe wystąpienie Derived
klasy . W Main()
pliku przykład tworzy wystąpienie Func<Base, Derived>
klasy (Func(Of Base, Derived)
w Visual Basic), które reprezentuje MyMethod
element , 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 |