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

Consulte também