Compartilhar via


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érico dão suporte à covariância e à contravariância para proporcionar maior flexibilidade na atribuição e no uso de tipos genéricos.

Quando você estiver se referindo a um sistema de tipos, covariância, contravariância e invariância têm as definições a seguir. 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> 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> uma variável do tipo Action<Derived>.

  • Invariance

    Significa que você pode usar apenas o tipo especificado originalmente. Um parâmetro de tipo genérico invariável não é covariante nem contravariante.

    Você não pode atribuir uma instância de List<Base> 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, portanto List<Derived> (List(Of Derived) no Visual Basic) implementa IEnumerable<Derived>. O parâmetro de tipo covariante faz o restante.

Por outro lado, a contravariância parece contraintuitiva. O exemplo a seguir cria um delegado de tipo Action<Base> (Action(Of Base) no Visual Basic) e 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 ser um retrocesso, mas é esse código de tipo seguro 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 de tipo Base e que não tem nenhum valor retornado. O delegado resultante pode ser atribuído a uma variável do tipo Action<Derived> porque o tipo de parâmetro T do delegado Action<T> é contravariante. O código é type-safe 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, pois 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 contravariantes podem ser usados como os tipos de parâmetro dos métodos da interface.

Covariância e contravariância são coletivamente conhecidas como variação. Um parâmetro de tipo genérico que não está marcado como covariante ou contravariante é conhecido como invariável. Um breve resumo de fatos sobre variância em Common Language Runtime:

  • Os parâmetros de tipo variant são restritos a interfaces genéricas e tipos de delegado genéricos.

  • Uma interface genérica ou um tipo de delegado genérico pode ter parâmetros de tipo covariantes e contravariantes.

  • A variação se aplica somente aos tipos de referência; se você especificar um tipo de valor para um parâmetro de tipo variante, esse parâmetro de tipo será invariável 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, embora o resultado seja tipo seguro. A variação permite que o segundo delegado seja atribuído a uma variável de tipo Action<Derived>, mas os delegados só poderão 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 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 covariante

Várias interfaces genéricas têm parâmetros de tipo covariante, por exemplo, IEnumerable<T>, e IEnumerator<T>IQueryable<T>IGrouping<TKey,TElement>. Todos os parâmetros de tipo dessas interfaces são covariantes, portanto, 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 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>, que tem um único parâmetro de tipo covariante. O parâmetro de tipo covariante é o motivo pelo qual uma instância 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 contravariante

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 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 baseia-se 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 objetos Circle usando um construtor que usa 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 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 Circle objeto é adicionado ao SortedSet<Circle>, o método IComparer<Shape>.Compare (IComparer(Of Shape).Compare método no Visual Basic) do objeto ShapeAreaComparer é chamado sempre 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 é 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 variante

Os Func delegados genéricos, como Func<T,TResult>, têm tipos de retorno covariantes e tipos de parâmetro contravariantes. Os Action delegados genéricos, como Action<T1,T2>, têm tipos de parâmetro contravariantes. Isso significa que os delegados podem ser atribuídos a variáveis que têm tipos de parâmetro mais derivados e (no caso dos delegados Func genéricos) tipos de retorno menos derivados.

Observação

O último parâmetro de tipo genérico dos Func delegados genéricos especifica o tipo do valor retornado na assinatura delegada. É covariante (out palavra-chave), enquanto os outros parâmetros de tipo genérico são contravariantes (in palavra-chave).

O código a seguir ilustra isso. A primeira parte do código define uma classe chamada Base, uma classe chamada Derived que herda Basee outra classe com um static método (Shared no Visual Basic) chamado MyMethod. O método recebe 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 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 MyMethode armazena-a 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 de 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 de 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 de 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 tipos de parâmetro mais derivados e tipos de retorno menos derivados, desde que todos os tipos delegados sejam construídos a partir do tipo Func<T,TResult>delegado genérico.

Esse é um ponto 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 variação na associação de delegados permite que um método seja associado a qualquer delegado que tenha tipos de parâmetro mais restritivos e um tipo de retorno menos restritivo, enquanto a atribuição de delegados genéricos só funcionará se ambos os tipos delegados forem construídos a partir da mesma definição de tipo genérico.

O exemplo a seguir mostra os efeitos combinados da variação na associação delegada e na variação em parâmetros de tipo genérico. O exemplo define uma hierarquia de tipo que inclui três tipos, do menos derivado (Type1) ao mais derivado (Type3). A variação na associação de delegados comuns é usada para associar um método com um tipo de parâmetro Type1 e um tipo de retorno Type3 a um delegado 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 tem um parâmetro de tipo Type3 e um tipo de retorno Type1, usando a covariância e a contravariância dos parâmetros de tipo genérico. A segunda atribuição requer que o tipo de variável e o tipo 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érico de interfaces e delegados como covariantes ou contravariantes.

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 retornado 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 tiver um parâmetro que seja um tipo delegado genérico, um parâmetro de tipo covariante do tipo de interface poderá 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érico para um método de interface.

Somente tipos de interface e tipos de delegado podem ter parâmetros de tipo variante. Um tipo de interface ou delegado pode ter parâmetros de tipo covariantes e contravariantes.

O Visual Basic e o C# não permitem que você viole as regras para usar parâmetros de tipo covariantes e contravariantes ou adicione anotações de covariância e contravariância aos parâmetros de tipo de tipos que não sejam interfaces e delegados.

Para obter informações e código de exemplo, consulte Variação em Interfaces Genéricas (C#) e Variação em Interfaces Genéricas (Visual Basic).

Lista de tipos

Os tipos de interface e delegado a seguir têm parâmetros de tipo covariantes e/ou contravariantes.

Tipo Parâmetros de tipo covariante Parâmetros de tipo contravariantes
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