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 tipo IEnumerable<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 tipo Action<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 tipo List<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> e Action<Base> (Action(Of Derived) e Action(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 tipo Action<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

Confira também