Covariância e contravariância em genéricos
Covariância e contravariância são termos que se referem à capacidade de usar um tipo mais derivado (mais específico) ou um tipo menos derivado (menos específico) do que o especificado originalmente. Os parâmetros de tipo genéricos suportam covariância e contravariância para fornecer maior flexibilidade na atribuição e uso de tipos genéricos.
Quando você está se referindo a um sistema de tipos, covariância, contravariância e invariância têm as seguintes definições. Os exemplos pressupõem uma classe base nomeada Base
e uma classe derivada chamada Derived
.
Covariance
Permite que você use um tipo mais derivado do que o especificado originalmente.
Você pode atribuir uma instância de
IEnumerable<Derived>
a uma variável do tipoIEnumerable<Base>
.Contravariance
Permite que você use um tipo mais genérico (menos derivado) do que o especificado originalmente.
Você pode atribuir uma instância de
Action<Base>
a uma variável do tipoAction<Derived>
.Invariance
Significa que você pode usar apenas o tipo originalmente especificado. Um parâmetro de tipo genérico invariante não é covariante nem contravariante.
Não é possível atribuir uma instância de
List<Base>
a uma variável do tipoList<Derived>
ou vice-versa.
Os parâmetros de tipo covariante permitem que você faça atribuições que se parecem muito com o Polimorfismo comum, conforme mostrado no código a seguir.
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
A List<T> classe implementa a IEnumerable<T> interface, então List<Derived>
(List(Of Derived)
no Visual Basic) implementa IEnumerable<Derived>
. O parâmetro de tipo covariante faz o resto.
A contravariância, por outro lado, parece contraintuitiva. O exemplo a seguir cria um delegado do tipo Action<Base>
(Action(Of Base)
no Visual Basic) e, em seguida, atribui esse delegado a uma variável do tipo 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())
Isso parece retroceder, mas é um código seguro para tipos que compila e executa. A expressão lambda corresponde ao delegado ao qual está atribuída, portanto, define um método que usa um parâmetro do tipo Base
e que não tem valor de retorno. O delegado resultante pode ser atribuído a uma variável do tipo Action<Derived>
porque o parâmetro T
type do Action<T> delegado é contravariante. O código é seguro para tipos porque T
especifica um tipo de parâmetro. Quando o delegado do tipo Action<Base>
é invocado como se fosse um delegado do tipo Action<Derived>
, seu argumento deve ser do tipo Derived
. Esse argumento sempre pode ser passado com segurança para o método subjacente, porque o parâmetro do método é do tipo Base
.
Em geral, um parâmetro de tipo covariante pode ser usado como o tipo de retorno de um delegado, e parâmetros de tipo contravariante podem ser usados como tipos de parâmetro. Para uma interface, parâmetros de tipo covariante podem ser usados como os tipos de retorno dos métodos da interface, e parâmetros de tipo contravariante podem ser usados como os tipos de parâmetros dos métodos da interface.
Covariância e contravariância são coletivamente referidas como variância. Um parâmetro de tipo genérico que não é marcado como covariante ou contravariante é referido como invariante. Um breve resumo dos fatos sobre a variância no common language runtime:
Os parâmetros de tipo de variante são restritos a interface genérica e tipos delegados genéricos.
Uma interface genérica ou um tipo de delegado genérico pode ter parâmetros de tipo covariante e contravariante.
A variância aplica-se apenas aos tipos de referência; Se você especificar um tipo de valor para um parâmetro de tipo de variante, esse parâmetro de tipo será invariante para o tipo construído resultante.
A variância não se aplica à combinação delegada. Ou seja, dado dois delegados de tipos
Action<Derived>
eAction<Base>
(Action(Of Derived)
eAction(Of Base)
no Visual Basic), você não pode combinar o segundo delegado com o primeiro, embora o resultado seria tipo seguro. A variância permite que o segundo delegado seja atribuído a uma variável do tipoAction<Derived>
, mas os delegados só podem combinar se seus tipos corresponderem exatamente.A partir do C# 9, há suporte para tipos de retorno covariantes. Um método de substituição pode declarar um tipo de retorno mais derivado o método que ele substitui, e uma propriedade somente leitura de substituição pode declarar um tipo mais derivado.
Interfaces genéricas com parâmetros de tipo covariante
Várias interfaces genéricas têm parâmetros de tipo covariante, por exemplo, IEnumerable<T>, , IEnumerator<T>IQueryable<T>, e IGrouping<TKey,TElement>. Todos os parâmetros de tipo dessas interfaces são covariantes, de modo que os parâmetros de tipo são usados apenas para os tipos de retorno dos membros.
O exemplo a seguir ilustra parâmetros de tipo covariante. O exemplo define dois tipos: Base
tem um método estático chamado PrintBases
que usa um IEnumerable<Base>
(IEnumerable(Of Base)
no Visual Basic) e imprime os elementos. Derived
herda de Base
. O exemplo cria um vazio List<Derived>
(List(Of Derived)
no Visual Basic) e demonstra que esse tipo pode ser passado e atribuído a PrintBases
uma variável de tipo IEnumerable<Base>
sem transmissão. List<T> implementa IEnumerable<T>, que tem um único parâmetro de tipo covariante. O parâmetro de tipo covariante é a razão pela qual uma instância de pode ser usada em vez de 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;
}
}
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
Interfaces genéricas com parâmetros de tipo contravariantes
Várias interfaces genéricas têm parâmetros de tipo contravariante; por exemplo: IComparer<T>, IComparable<T>, e IEqualityComparer<T>. Essas interfaces têm apenas parâmetros de tipo contravariante, de modo que os parâmetros de tipo são usados apenas como tipos de parâmetros nos membros das interfaces.
O exemplo a seguir ilustra parâmetros de tipo contravariante. O exemplo define uma classe abstrata (MustInherit
no Visual Basic) Shape
com uma Area
propriedade. O exemplo também define uma ShapeAreaComparer
classe que implementa IComparer<Shape>
(IComparer(Of Shape)
no Visual Basic). A implementação do IComparer<T>.Compare método é baseada no valor da Area
propriedade, portanto ShapeAreaComparer
, pode ser usada para classificar Shape
objetos por área.
A Circle
classe herda Shape
e substitui Area
. O exemplo cria um SortedSet<T> de Circle
objetos, usando um construtor que usa um IComparer<Circle>
(IComparer(Of Circle)
no Visual Basic). No entanto, em vez de passar um IComparer<Circle>
, o exemplo passa um ShapeAreaComparer
objeto, que implementa IComparer<Shape>
. O exemplo pode passar um comparador de um tipo menos derivado (Shape
) quando o código chama um comparador de um tipo mais derivado (Circle
), porque o IComparer<T> parâmetro type da interface genérica é contravariante.
Quando um novo Circle
objeto é adicionado ao SortedSet<Circle>
, o IComparer<Shape>.Compare
método (IComparer(Of Shape).Compare
método no Visual Basic) do ShapeAreaComparer
objeto é chamado cada vez que o novo elemento é comparado a um elemento existente. O tipo de parâmetro do método (Shape
) é menos derivado do que o tipo que está sendo passado (Circle
), portanto, a chamada é segura do tipo. A contravariância permite ShapeAreaComparer
classificar uma coleção de qualquer tipo único, bem como uma coleção mista de tipos, que derivam de 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
Delegados genéricos com parâmetros de tipo variante
Os Func
delegados genéricos, como Func<T,TResult>, têm tipos de retorno covariantes e tipos de parâmetros contravariantes. Os Action
delegados genéricos, como Action<T1,T2>, têm tipos de parâmetros contravariantes. Isso significa que os delegados podem ser atribuídos a variáveis que têm mais tipos de parâmetros derivados e (no caso dos delegados genéricos) menos tipos de Func
retorno derivados.
Nota
O último parâmetro de tipo genérico dos Func
delegados genéricos especifica o tipo do valor de retorno na assinatura do delegado. É covariante (out
palavra-chave), enquanto os outros parâmetros de tipo genéricos são contravariantes (in
palavra-chave).
O seguinte código ilustra-o. A primeira parte do código define uma classe chamada Base
, uma classe nomeada Derived
que herda Base
e outra classe com um static
método (Shared
no Visual Basic) chamado MyMethod
. O método usa uma instância de Base
e retorna uma instância de Derived
. (Se o argumento for uma instância de Derived
, MyMethod
retorna-o; se o argumento for uma instância de Base
, MyMethod
retorna uma nova instância de Derived
.) No Main()
, o exemplo cria uma instância de Func<Base, Derived>
(Func(Of Base, Derived)
no Visual Basic) que representa MyMethod
e a armazena na variável 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
A segunda parte do código mostra que o delegado pode ser atribuído a uma variável do tipo Func<Base, Base>
(Func(Of Base, Base)
no Visual Basic), porque o tipo de retorno é covariante.
// 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())
A terceira parte do código mostra que o delegado pode ser atribuído a uma variável do tipo Func<Derived, Derived>
(Func(Of Derived, Derived)
no Visual Basic), porque o tipo de parâmetro é contravariante.
// 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())
A parte final do código mostra que o delegado pode ser atribuído a uma variável do tipo Func<Derived, Base>
(Func(Of Derived, Base)
no Visual Basic), combinando os efeitos do tipo de parâmetro contravariante e o tipo de retorno covariante.
// 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())
Variação em delegados não genéricos
No código anterior, a assinatura de MyMethod
corresponde exatamente à assinatura do delegado genérico construído: Func<Base, Derived>
(Func(Of Base, Derived)
no Visual Basic). O exemplo mostra que esse delegado genérico pode ser armazenado em variáveis ou parâmetros de método que têm mais tipos de parâmetros derivados e menos tipos de retorno derivados, desde que todos os tipos de delegado sejam construídos a partir do tipo Func<T,TResult>de delegado genérico.
Este é um ponto importante. Os efeitos de covariância e contravariância nos parâmetros de tipo de delegados genéricos são semelhantes aos efeitos de covariância e contravariância na vinculação de delegados ordinários (consulte Variância em delegados (C#) e Variância em delegados (Visual Basic)). No entanto, a variação na vinculação de delegados funciona com todos os tipos de delegados, não apenas com tipos de delegados genéricos que têm parâmetros de tipo de variante. Além disso, a variância na vinculação de delegados permite que um método seja vinculado a qualquer delegado que tenha tipos de parâmetros mais restritivos e um tipo de retorno menos restritivo, enquanto a atribuição de delegados genéricos funciona somente se ambos os tipos de delegados forem construídos a partir da mesma definição de tipo genérico.
O exemplo a seguir mostra os efeitos combinados de variância na ligação delegada e variância em parâmetros de tipo genérico. O exemplo define uma hierarquia de tipos que inclui três tipos, do menos derivado (Type1
) ao mais derivado (Type3
). A variância na vinculação de delegado comum é usada para vincular um método com um tipo de parâmetro e Type1
um tipo de retorno de Type3
a um delegado genérico com um tipo de parâmetro de e um tipo de Type2
retorno de Type2
. O delegado genérico resultante é então atribuído a outra variável cujo tipo de delegado genérico tem um parâmetro de tipo Type3
e um tipo de retorno de Type1
, usando a covariância e contravariância de parâmetros de tipo genérico. A segunda atribuição requer que tanto o tipo de variável quanto o tipo de delegado sejam construídos a partir da mesma definição genérica de tipo, neste caso, 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
Definir interfaces genéricas de variantes e delegados
Visual Basic e C# têm palavras-chave que permitem marcar os parâmetros de tipo genéricos de interfaces e delegados como covariante ou contravariante.
Um parâmetro de tipo covariante é marcado com a out
palavra-chave (Out
palavra-chave no Visual Basic). Você pode usar um parâmetro de tipo covariante como o valor de retorno de um método que pertence a uma interface ou como o tipo de retorno de um delegado. Não é possível usar um parâmetro de tipo covariante como uma restrição de tipo genérica para métodos de interface.
Nota
Se um método de uma interface tem um parâmetro que é um tipo de delegado genérico, um parâmetro de tipo covariante do tipo de interface pode ser usado para especificar um parâmetro de tipo contravariante do tipo delegado.
Um parâmetro de tipo contravariante é marcado com a in
palavra-chave (In
palavra-chave no Visual Basic). Você pode usar um parâmetro de tipo contravariante como o tipo de um parâmetro de um método que pertence a uma interface ou como o tipo de um parâmetro de um delegado. Você pode usar um parâmetro de tipo contravariante como uma restrição de tipo genérica para um método de interface.
Somente tipos de interface e tipos delegados podem ter parâmetros de tipo variante. Uma interface ou tipo de delegado pode ter parâmetros de tipo covariante e contravariante.
Visual Basic e C# não permitem que você viole as regras para usar parâmetros de tipo covariante e contravariante, ou para adicionar anotações de covariância e contravariância para os parâmetros de tipo de tipos diferentes de interfaces e delegados.
Para obter informações e código de exemplo, consulte Variância em interfaces genéricas (C#) e Variância em interfaces genéricas (Visual Basic).
Lista de tipos
Os seguintes tipos de interface e delegados têm parâmetros de tipo covariante e/ou contravariante.
Type | Parâmetros de tipo covariante | Parâmetros do tipo contravariante |
---|---|---|
Action<T> a Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16> | Sim | |
Comparison<T> | Sim | |
Converter<TInput,TOutput> | Sim | Sim |
Func<TResult> | Sim | |
Func<T,TResult> a Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult> | Sim | Sim |
IComparable<T> | Sim | |
Predicate<T> | Sim | |
IComparer<T> | Sim | |
IEnumerable<T> | Sim | |
IEnumerator<T> | Sim | |
IEqualityComparer<T> | Sim | |
IGrouping<TKey,TElement> | Sim | |
IOrderedEnumerable<TElement> | Sim | |
IOrderedQueryable<T> | Sim | |
IQueryable<T> | Sim |