Covariância e contravariância em genéricos
Covariância e contravariância são termos que fazem referência à capacidade de usar um tipo mais derivado (mais específico) ou menos derivado (menos específico) do que o especificado originalmente. Os parâmetros de tipo genéricos oferecem suporte a covariância e contravariância para fornecer maior flexibilidade na atribuição e no uso de tipos genéricos.
Quando você se refere a um sistema de tipos, a covariância, contravariância e a invariância têm as definições a seguir. Os exemplos assumem uma classe base chamada Base
e uma classe derivada chamada Derived
.
Covariance
Permite usar um tipo mais derivado que o especificado originalmente.
Você pode atribuir uma instância de
IEnumerable<Derived>
uma variável do tipoIEnumerable<Base>
.Contravariance
Permite a você usar um tipo mais genérico (menos derivado) do que aquele especificado originalmente.
Você pode atribuir uma instância de
Action<Base>
uma variável do tipoAction<Derived>
.Invariance
Significa que você só pode usar o tipo especificado originalmente. Um parâmetro de tipo genérico invariante não é covariante nem contravariante.
Você não pode atribuir uma instância de
List<Base>
a uma variável do tipoList<Derived>
, ou vice-versa.
Os parâmetros de tipo covariantes permitem fazer atribuições muito semelhantes ao 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 classe List<T> implementa a interface IEnumerable<T>. Assim, 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 não ser intuitiva. O exemplo a seguir cria um delegado do tipo Action<Base>
(Action(Of Base)
no Visual Basic) e, em seguida, atribuir o 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 ser um retrocesso, mas é esse código de tipo seguro que compila e executa. A expressão lambda corresponde ao delegado ao qual ela é atribuída. Assim, ela define um método que recebe um parâmetro do tipo Base
e não tem nenhum valor retornado. O delegado resultante pode ser atribuído a uma variável do tipo Action<Derived>
porque o parâmetro de tipo T
do delegado Action<T> é contravariante. O código é de tipo seguro 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
. Este argumento sempre pode ser passado ao método subjacente com segurança porque o parâmetro do método é do tipo Base
.
Geralmente, um parâmetro de tipo de covariante pode ser usado como o tipo de retorno de um delegado, e os parâmetros de tipo contravariant podem ser usados como tipos de parâmetro. Para uma interface, os parâmetros de tipo covariantes podem ser usados como os tipos de retorno dos métodos da interface, e os parâmetros de tipo contravariantes podem ser usados como os tipos de parâmetro dos métodos da interface.
A covariância e a contravariância são referidas coletivamente como variância. Um parâmetro de tipo genérico que não é covariante ou contravariante é referido como invariante. Um breve resumo de fatos sobre variância em Common Language Runtime:
Os parâmetros de tipo variantes são restringidos à interface genérica e tipos de delegados genéricos.
Uma interface genérica ou um tipo delegado genérico podem ter parâmetros de tipo covariantes e contravariantes.
A variância aplica-se apenas para referenciar tipos; se você especificar um tipo de valor para um parâmetro de tipo variante, esse parâmetro de tipo será invariante para o tipo construído resultante.
A variância não se aplica à combinação de delegado. Ou seja, considerando 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, ainda que o resultado seja de tipo seguro. A variância permite que o segundo delegado seja atribuído a uma variável do tipoAction<Derived>
, mas os delegados podem ser combinados somente quando seus tipos são exatamente iguais.Do C# 9 em diante, há suporte para tipos de retorno covariantes. Um método de substituição pode declarar um tipo de retorno mais derivado do 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 covariantes
Várias interfaces genéricas têm parâmetros de tipo covariantes, por exemplo: IEnumerable<T>, IEnumerator<T>, IQueryable<T> e IGrouping<TKey,TElement>. Todos os parâmetros de tipo dessas interfaces são covariantes, de forma 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 covariantes. 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 List<Derived>
vazio (List(Of Derived)
no Visual Basic) e demonstra que esse tipo pode ser passado a PrintBases
e atribuído a uma variável do tipo IEnumerable<Base>
sem converter. List<T> implementa IEnumerable<T>, o qual tem um único parâmetro de tipo covariante. O parâmetro de tipo covariante é a razão pela qual uma instância de IEnumerable<Derived>
pode ser usada em vez de 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 contravariantes, por exemplo: IComparer<T>, IComparable<T> e IEqualityComparer<T>. Essas interfaces possuem apenas parâmetros de tipo contravariantes. Assim, os parâmetros de tipo são usados apenas como parâmetros de tipo nos membros das interfaces.
O exemplo a seguir ilustra parâmetros de tipo contravariantes. O exemplo define uma classe abstrata (MustInherit
no Visual Basic) Shape
com uma propriedade Area
. O exemplo também define uma classe ShapeAreaComparer
que implementa IComparer<Shape>
(IComparer(Of Shape)
no Visual Basic). A implementação do método IComparer<T>.Compare baseia-se no valor da propriedade Area
, portanto ShapeAreaComparer
pode ser usado para classificar objetos Shape
por área.
A classe Circle
herda Shape
e substitui Area
. O exemplo cria um SortedSet<T> de objetos Circle
usando um construtor que usa IComparer<Circle>
(IComparer(Of Circle)
no Visual Basic). Porém, em vez de passar um IComparer<Circle>
, o exemplo passa um objeto ShapeAreaComparer
que implementa IComparer<Shape>
. O exemplo pode passar um comparador de um tipo derivado (Shape
) quando um código chama um comparador de um tipo mais derivado (Circle
) porque o parâmetro de tipo da interface genérica IComparer<T> é contravariante.
Quando um novo objeto Circle
é adicionado a SortedSet<Circle>
, o método IComparer<Shape>.Compare
(método IComparer(Of Shape).Compare
no Visual Basic) do objeto ShapeAreaComparer
é chamado sempre que um novo elemento é comparado a um elemento existente. O tipo de parâmetro do método (Shape
) é menos derivado que o tipo que está sendo passado (Circle
). Assim, a chamada é de tipo seguro. A contravariância permite que ShapeAreaComparer
classifique uma coleção de um único tipo, bem como uma coleção mista de tipos, que deriva 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 variantes
Os delegados genéricos Func
, como Func<T,TResult>, têm tipos de retorno covariantes e tipos de parâmetro contravariantes. Os delegados genéricos Action
, como Action<T1,T2>, possuem tipos de parâmetros contravariantes. Isso significa que os delegados podem ser atribuídos a variáveis que possuem tipos de parâmetro mais derivados e (no caso dos delegados genéricos Func
) menos tipos de retorno derivados.
Observação
O último parâmetro de tipo genérico dos delegados genéricos Func
especifica o tipo do valor de retorno na assinatura do delegado. É covariante (palavra-chave out
), enquanto os outros parâmetros de tipo genéricos são contravariantes (palavra-chave in
).
O código a seguir ilustra isso. O primeiro trecho de código define uma classe chamada Base
, uma classe chamada Derived
que herda Base
e outra classe com um método static
(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
o retornará; se o argumento for uma instância de Base
, MyMethod
retornará uma nova instância de Derived
.) Em 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
O segundo trecho de código a seguir 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())
O terceiro trecho de código a seguir 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())
O trecho final de código mostra que o delegado pode ser atribuído a uma variável de tipo Func<Derived, Base>
(Func(Of Derived, Base)
no Visual Basic), combinando os efeitos do tipo de parâmetro contravariante e do 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())
Variância 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 do método que têm tipos de parâmetro mais derivados e tipos de retorno menos derivados, desde que todos os tipos de delegados sejam construídos do tipo de delegado genérico Func<T,TResult>.
Este é um aspecto importante. Os efeitos da covariância e da contravariância nos parâmetros de tipo de delegados genéricos são semelhantes aos efeitos da covariância e da contravariância na associação comum de delegação (confira Variância em delegados (C#) e Variância em delegados (Visual Basic)). No entanto, a variância na associação de delegados funciona com todos os tipos de delegados, e não apenas com tipos de delegados genéricos com parâmetros de tipo variantes. Além disso, a variância na associação de delegados possibilita a um método a ser associado a qualquer delegado que tenha os tipos de parâmetro mais restritivos e um tipo de retorno menos restritivo, enquanto que a atribuição de delegados genéricos só funciona quando ambos os tipos de delegados são construídos da mesma definição de tipo genérico.
O exemplo a seguir mostra os efeitos combinados da variância na associação de delegados e a variância em parâmetros de tipo genéricos. O exemplo define uma hierarquia de tipo que inclui três tipos, do menos derivado (Type1
) para o mais derivado (Type3
). A variância na associação comum de delegados é usada para associar um método a um tipo de parâmetro Type1
e um tipo de retorno Type3
a um representante genérico com um tipo de parâmetro Type2
e um tipo de retorno Type2
. O delegado genérico resultante é então atribuído a outra variável cujo tipo de delegado genérico possui um parâmetro de tipo Type3
e tipo de retorno Type1
, usando a covariância e a contravariância de parâmetros de tipo genéricos. A segunda atribuição requer que o tipo de variável e o tipo de delegado sejam construídos a partir da mesma definição de tipo genérico, nesse 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 delegados e interfaces genéricas variantes
O Visual Basic e o C# têm palavras-chave que permitem marcar os parâmetros de tipo genéricos de interfaces e delegados como covariantes ou contravariantes.
Um parâmetro de tipo de covariante é marcado com a palavra-chave out
(palavra-chave Out
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. Você não pode usar um parâmetro de tipo covariante como uma restrição de tipo genérico para métodos de interface.
Observação
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 da interface pode ser usado para especificar um parâmetro de tipo contravariante do tipo delegado.
Um parâmetro de tipo contracovariante é marcado com a palavra-chave in
(palavra-chave In
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érico para um método de interface.
Somente tipos de interfaces e tipos de delegados podem ter parâmetros de tipo variantes. Um tipo de delegado ou interface pode ter parâmetros de tipo covariantes e contravariantes.
O Visual Basic e o C# e não permitem que você viole as regras de uso de parâmetros de tipo covariantes e contravariantes nem adicionar anotações de covariância e de contravariância aos parâmetros de tipo de tipos que não sejam interfaces e delegados.
Para obter mais informações e códigos de exemplo, confira Variação em interfaces genéricas (C#) e Variação em interfaces genéricas (Visual Basic).
Lista de tipos
Os tipos de interface e delegados a seguir têm parâmetros de tipo covariantes e/ou contravariantes.
Tipo | Parâmetros de tipo covariantes | Parâmetros de tipo contravariantes |
---|---|---|
Action<T> em 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> em 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 |