Covarianza e controvarianza nei generics

Covarianza e controvarianza sono termini che fanno riferimento alla possibilità di usare un tipo più derivato (più specifico) o un tipo meno derivato (meno specifico) di quanto specificato in origine. I parametri di tipo generico supportano la covarianza e la controvarianza per offrire la massima flessibilità nell'assegnazione e nell'utilizzo dei tipi generici.

Quando si fa riferimento a un sistema di tipi, la covarianza, la controvarianza e l'invarianza hanno le seguenti definizioni. Negli esempi si presuppone una classe di base denominata Base e una classe derivata denominata Derived.

  • Covariance

    Permette di usare un tipo più derivato di quello originariamente specificato.

    È possibile assegnare un'istanza di IEnumerable<Derived> a una variabile di tipo IEnumerable<Base>.

  • Contravariance

    Consente di utilizzare un tipo più generico (meno derivato) di quello originariamente specificato.

    È possibile assegnare un'istanza di Action<Base> a una variabile di tipo Action<Derived>.

  • Invariance

    Significa che è possibile usare solo il tipo specificato in origine. Un parametro di tipo generico invariante non è né covariante né controvariante.

    Non è possibile assegnare un'istanza di List<Base> a una variabile di tipo List<Derived> o viceversa.

I parametri di tipo covariante consentono di effettuare assegnazioni simili al polimorfismo ordinario., come illustrato nel codice seguente.

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

La classe List<T> implementa l'interfaccia IEnumerable<T> , pertanto List<Derived> (List(Of Derived) in Visual Basic) implementa IEnumerable<Derived>. Il parametro di tipo covariante completa l'operazione.

Viceversa, la controvarianza è poco intuitiva. Nell'esempio seguente viene creato un delegato di tipo Action<Base> (Action(Of Base) in Visual Basic), che viene quindi assegnato a una variabile di 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())

Può sembrare un passo indietro, ma si tratta di codice indipendente dai tipi che viene compilato ed eseguito. L'espressione lambda corrisponde al delegato a cui è assegnata, pertanto definisce un metodo che accetta un parametro di tipo Base senza valore restituito. Il delegato risultante può essere assegnato a una variabile di tipo Action<Derived> poiché il parametro di tipo T del delegato Action<T> è controvariante. Il codice è indipendente dai tipi perché T specifica un tipo di parametro. Quando il delegato di tipo Action<Base> viene richiamato come se fosse un delegato di tipo Action<Derived>, il relativo argomento deve essere di tipo Derived. Questo argomento può sempre essere passato in modo sicuro al metodo sottostante, perché il parametro del metodo è di tipo Base.

In genere, un parametro di tipo covariante può essere utilizzato come tipo restituito di un delegato e i parametri di tipo controvariante possono essere utilizzati come tipi di parametri. Per un'interfaccia, i parametri di tipo covariante possono essere utilizzati come tipi restituiti dei metodi dell'interfaccia e i parametri di tipo controvariante possono essere utilizzati come tipi di parametri dei metodi dell'interfaccia.

La covarianza e la controvarianza sono definite collettivamente varianza. Un parametro di tipo generico non contrassegnato come covariante o controvariante viene definito invariante. Di seguito vengono riepilogati i concetti relativi alla varianza in Common Language Runtime:

  • I parametri di tipo variante sono limitati ai tipi di interfaccia generica e delegato generico.

  • Un tipo di interfaccia generica o delegato generico può presentare parametri di tipo sia covariante sia controvariante.

  • La varianza si applica solo ai tipi di riferimento. Se si specifica un tipo di valore per un parametro di tipo variante, tale parametro di tipo è invariante per il tipo costruito risultante.

  • La varianza non si applica alla combinazione di delegati. Ciò significa che nel caso di due delegati di tipo Action<Derived> e Action<Base> (Action(Of Derived) e Action(Of Base) in Visual Basic), non è possibile combinare il secondo delegato con il primo anche se il risultato sarebbe indipendente dai tipi. La varianza consente l'assegnazione del secondo delegato a una variabile di tipo Action<Derived>, ma i delegati possono essere combinati solo se il loro tipo corrisponde esattamente.

  • A partire da C# 9, sono supportati i tipi restituiti covarianti. Un metodo che esegue l’override può dichiarare un tipo restituito più derivato il metodo di cui esegue l'override e una proprietà di sola lettura che esegue l’override può dichiarare un tipo più derivato.

Interfacce generiche con parametri di tipo covariante

Diverse interfacce generiche presentano parametri di tipo controvariante, ad esempio: IEnumerable<T>, IEnumerator<T>, IQueryable<T>e IGrouping<TKey,TElement>. Tutti i parametri del tipo di queste interfacce sono covarianti, pertanto i parametri del tipo vengono utilizzati solo per i tipi restituiti dei membri.

Nell'esempio seguente vengono illustrati parametri di tipo covariante. Nell'esempio vengono definiti due tipi: Base presenta un metodo statico denominato PrintBases che accetta IEnumerable<Base> (IEnumerable(Of Base) in Visual Basic) e stampa gli elementi. Derived eredita da Base. Nell'esempio viene creato un tipo List<Derived> (List(Of Derived) in Visual Basic) vuoto e viene illustrato che è possibile passare tale tipo a PrintBases e assegnarlo a una variabile di tipo IEnumerable<Base> senza eseguire il cast. List<T> implementa IEnumerable<T>, che dispone di un solo parametro di tipo covariante. Il parametro di tipo covariante è il motivo per cui è possibile usare un'istanza di IEnumerable<Derived> anziché di 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

Interfacce generiche con parametri di tipo controvariante

Diverse interfacce generiche presentano parametri di tipo controvariante, ad esempio: IComparer<T>, IComparable<T> e IEqualityComparer<T>. Queste interfacce dispongono solo di parametri di tipo controvariante, pertanto i parametri di tipo vengono utilizzati solo come tipi di parametri nei membri delle interfacce.

Nell'esempio seguente vengono illustrati parametri di tipo controvariante. Nell'esempio viene definita una classeMustInherit astratta ( Shape in Visual Basic) con una proprietà Area . Nell'esempio viene definita anche una classe ShapeAreaComparer che implementa IComparer<Shape> (IComparer(Of Shape) in Visual Basic). L'implementazione del metodo IComparer<T>.Compare è basata sul valore della proprietà Area , pertanto ShapeAreaComparer può essere utilizzato per ordinare gli oggetti Shape in base all'area.

La classe Circle eredita dalla classe Shape ed esegue l'override di Area. Nell'esempio viene creato un oggetto SortedSet<T> di oggetti Circle , usando un costruttore che accetta un oggetto IComparer<Circle> (IComparer(Of Circle) in Visual Basic). Tuttavia, anziché passare un oggetto IComparer<Circle>, nell'esempio viene passato un oggetto ShapeAreaComparer che implementa IComparer<Shape>. È possibile passare un operatore di confronto di un tipo meno derivato (Shape) quando il codice chiama un operatore di confronto di un tipo più derivato (Circle), poiché il parametro di tipo dell'interfaccia generica IComparer<T> è controvariante.

Quando un nuovo oggetto Circle viene aggiunto all'oggetto SortedSet<Circle>, il metodo IComparer<Shape>.Compare (metodoIComparer(Of Shape).Compare in Visual Basic) dell'oggetto ShapeAreaComparer viene chiamato ogni volta che il nuovo elemento viene confrontato con un elemento esistente. Il tipo di parametro del metodo (Shape) è meno derivato rispetto al tipo passato (Circle), pertanto la chiamata è indipendente dai tipi. La controvarianza consente a ShapeAreaComparer di ordinare una raccolta di qualsiasi singolo tipo nonché una raccolta mista di tipi che derivano da 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

Delegati generici con parametri di tipo variante

I delegati generici Func, ad esempio Func<T,TResult>, presentano tipi restituiti covariante e tipi di parametro controvariante. I delegati generici Action , ad esempio Action<T1,T2>, presentano tipi di parametro controvarianti. Ciò significa che i delegati possono essere assegnati a variabili che presentano tipi di parametro più derivati e (nel caso dei delegati generici Func) tipi restituiti meno derivati.

Nota

L'ultimo parametro di tipo generico dei delegati generici Func specifica il tipo del valore restituito nella firma del delegato. È covariante (parola chiaveout ), mentre gli altri parametri di tipo generico sono controvarianti (parola chiavein ).

Il codice seguente illustra questo concetto. Nella prima parte di codice vengono definite una classe denominata Base, una classe denominata Derived che eredita da Basee un'altra classe con un metodo static (Shared in Visual Basic) denominata MyMethod. Il metodo accetta un'istanza di Base e restituisce un'istanza di Derived. Se l'argomento è un'istanza di Derived, MyMethod la restituisce. Se l'argomento è un'istanza di Base, MyMethod restituisce una nuova istanza di Derived. In Main(), nell'esempio viene creata un'istanza di Func<Base, Derived> (Func(Of Base, Derived) in Visual Basic) che rappresenta MyMethod, quindi l'istanza viene archiviata nella variabile 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

Nella seconda parte di codice viene illustrato che è possibile assegnare il delegato a una variabile di tipo Func<Base, Base> (Func(Of Base, Base) in Visual Basic), poiché il tipo restituito è 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())

Nella terza parte di codice viene illustrato che è possibile assegnare il delegato a una variabile di tipo Func<Derived, Derived> (Func(Of Derived, Derived) in Visual Basic), poiché il tipo di parametro è controvariante.

// 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())

Nella parte finale di codice viene illustrato che è possibile assegnare il delegato a una variabile di tipo Func<Derived, Base> (Func(Of Derived, Base) in Visual Basic), combinando gli effetti del tipo di parametro controvariante e del tipo restituito 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())

Varianza in delegati non generici

Nel codice precedente la firma di MyMethod corrisponde esattamente alla firma del delegato generico costruito: Func<Base, Derived> (Func(Of Base, Derived) in Visual Basic). Nell'esempio viene illustrato che è possibile archiviare questo delegato generico in variabili o parametri del metodo che presentano tipi di parametro più derivati e tipi restituiti meno derivati, purché tutti i tipi delegati vengano costruiti a partire dal tipo delegato generico Func<T,TResult>.

Questo è un punto importante. Gli effetti della covarianza e della controvarianza nei parametri di tipo dei delegati generici sono simili agli effetti della covarianza e della controvarianza nel normale binding di delegati. Vedere Varianza nei delegati (C#) e Varianza nei delegati (Visual Basic). Tuttavia, la varianza nell'associazione di delegati funziona con tutti i tipi delegati, non solo con quelli generici aventi parametri di tipo variante. Inoltre, la varianza nell'associazione di delegati consente l'associazione di un metodo a qualsiasi delegato con tipi di parametro più restrittivi e un tipo restituito meno restrittivo, mentre l'assegnazione di delegati generici funziona solo se entrambi i tipi di delegati sono costruiti a partire dalla stessa definizione di tipo generico.

Nell'esempio seguente sono mostrati gli effetti combinati della varianza nell'associazione di delegati e della varianza nei parametri di tipo generico. Nell'esempio viene definita una gerarchia di tre tipi, da quello meno derivato (Type1) a quello più derivato (Type3). La varianza nella normale associazione di delegati viene utilizzata per associare un metodo con un tipo di parametro Type1 e un tipo restituito Type3 a un delegato generico con un tipo di parametro Type2 e un tipo restituito Type2. Il delegato generico risultante viene quindi assegnato a un'altra variabile il cui tipo delegato generico presenta un parametro di tipo Type3 e un tipo restituito Type1, utilizzando la covarianza e la controvarianza di parametri di tipo generico. Per la seconda assegnazione è necessario che sia il tipo della variabile sia quello del delegato vengano costruiti a partire dalla stessa definizione di tipo generico, in questo 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

Definire interfacce e delegati generici varianti

Visual Basic e C# presentano parole chiave che consentono di contrassegnare i parametri di tipo generico di interfacce e delegati come covarianti o controvarianti.

Un parametro di tipo covariante è contrassegnato con la parola chiave out (parola chiave Out in Visual Basic). È possibile usare un parametro di tipo covariante come valore restituito di un metodo che appartiene a un'interfaccia o come tipo restituito di un delegato. Non è possibile usare un parametro di tipo covariante come vincolo di tipo generico per i metodi di interfaccia.

Nota

Se un metodo di un'interfaccia presenta un parametro che è un tipo delegato generico, per specificare un parametro di tipo controvariante del tipo delegato è possibile usare un parametro di tipo covariante del tipo di interfaccia.

Un parametro di tipo controvariante è contrassegnato con la parola chiave in (parola chiaveIn in Visual Basic). È possibile usare un parametro di tipo controvariante come tipo di un parametro di un metodo che appartiene a un'interfaccia o come tipo di un parametro di un delegato. È possibile usare un parametro di tipo controvariante come vincolo di tipo generico per un metodo di interfaccia.

Solo i tipi di interfaccia e i tipi delegati possono presentare parametri di tipo variante. Un tipo di interfaccia o delegato può presentare parametri di tipo sia covariante sia controvariante.

Visual Basic e C# non consentono di violare le regole per l'utilizzo di parametri di tipo covariante e controvariante o aggiungere annotazioni di covarianza e controvarianza ai parametri relativi a tipi diversi da interfacce e delegati.

Per informazioni e codice di esempio, vedere Varianza nelle interfacce generiche (C#) e Varianza nelle interfacce generiche (Visual Basic).

Elenco di tipi

I seguenti tipi di interfaccia e delegato presentano parametri di tipo covariante e/o controvariante.

Type Parametri di tipo covariante Parametri di tipo controvariante
Action<T> a Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>
Comparison<T>
Converter<TInput,TOutput>
Func<TResult>
Func<T,TResult> a Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>
IComparable<T>
Predicate<T>
IComparer<T>
IEnumerable<T>
IEnumerator<T>
IEqualityComparer<T>
IGrouping<TKey,TElement>
IOrderedEnumerable<TElement>
IOrderedQueryable<T>
IQueryable<T>

Vedi anche