Covarianza e controvarianza nei generics
I parametri di tipo generico covarianti e controvarianti offrono la massima flessibilità nell'assegnazione e nell'utilizzo dei tipi generici. Ad esempio, i parametri di tipo covariante consentono di effettuare assegnazioni simili al polimorfismo ordinario. Si supponga di disporre di una classe di base e di una classe derivata, denominate Base e Derived. Il polimorfismo consente di assegnare un'istanza di Derived a una variabile di tipo Base. Allo stesso modo, poiché il parametro di tipo dell'interfaccia IEnumerable<T> è covariante, è possibile assegnare un'istanza di IEnumerable<Derived> (IEnumerable(Of Derived) in Visual Basic) a una variabile di tipo IEnumerable<Base>, come illustrato nel codice seguente.
Dim d As IEnumerable(Of Derived) = New List(Of Derived)
Dim b As IEnumerable(Of Base) = d
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = 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.
La covarianza è intuitiva perché è analoga al polimorfismo. 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>.
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())
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = 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 è assegnato, 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:
In .NET Framework versione 4, 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.
Nelle sottosezioni seguenti vengono descritti in dettaglio parametri di tipo covariante e controvariante:
Interfacce generiche con parametri di tipo covariante
Interfacce generiche con parametri di tipo generico controvariante
Delegati generici con parametri di tipo variante
Definizione di interfacce e delegati generici varianti
Elenco di tipi di interfacce e delegati generici varianti
Interfacce generiche con parametri di tipo covariante
A partire da .NET Framework 4, diverse interfacce generiche presentano parametri di tipo covariante, ad esempio IEnumerable<T>, IEnumerator<T>, IQueryable<T> e IGrouping<TKey, TElement>. Tutti i parametri di tipo di queste interfacce sono covarianti, pertanto i parametri di 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 utilizzare un'istanza di IEnumerable<Derived> anziché di IEnumerable<Base>.
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
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;
}
}
Torna all'inizio
Interfacce generiche con parametri di tipo generico controvariante
A partire da .NET Framework 4, 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 classe Shape astratta (MustInherit 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, utilizzando 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 (metodo IComparer(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 un insieme di qualsiasi singolo tipo nonché un insieme misto di tipi che derivano da Shape.
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
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
*/
Torna all'inizio
Delegati generici con parametri di tipo variante
In .NET Framework 4 i delegati generici Func, ad esempio Func<T, TResult>, presentano tipi restituiti covarianti e tipi di parametro controvarianti. 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 chiave out), mentre gli altri parametri di tipo generico sono controvarianti (parola chiave in). |
Questa condizione è illustrata nel codice che segue. Nella prima parte di codice vengono definite una classe denominata Base, una classe denominata Derived che eredita da Base e 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
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
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;
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.
Dim f2 As Func(Of Base, Base) = f1
Dim b2 As Base = f2(New Base())
// Covariant return type.
Func<Base, Base> f2 = f1;
Base b2 = 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.
Dim f3 As Func(Of Derived, Derived) = f1
Dim d3 As Derived = f3(New Derived())
// Contravariant parameter type.
Func<Derived, Derived> f3 = f1;
Derived d3 = 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.
Dim f4 As Func(Of Derived, Base) = f1
Dim b4 As Base = f4(New Derived())
// Covariant return type and contravariant parameter type.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());
Varianza nei delegati generici e 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 nella normale associazione di delegati (vedere Varianza nei delegati (C# e 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>.
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
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());
}
}
Torna all'inizio
Definizione di interfacce e delegati generici varianti
A partire da .NET Framework 4, Visual Basic e C# presentano parole chiave che consentono di contrassegnare i parametri di tipo generico di interfacce e delegati come covarianti o controvarianti.
Nota |
---|
A partire dalla versione 2.0 di .NET Framework, Common Language Runtime supporta le annotazioni di variante nei parametri di tipo generico.Nelle versioni precedenti a .NET Framework 4, l'unico modo per definire una classe generica avente queste annotazioni è utilizzare Microsoft Intermediate Language (MSIL), compilando la classe con Ilasm.exe (assembler MSIL) oppure generandola in un assembly dinamico. |
Un parametro di tipo covariante è contrassegnato con la parola chiave out (parola chiave Out in Visual Basic, + per l'assembler MSIL). È possibile utilizzare 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 utilizzare 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 utilizzare un parametro di tipo covariante del tipo di interfaccia. |
Un parametro di tipo controvariante è contrassegnato con la parola chiave in (parola chiave In in Visual Basic, - per l'assembler MSIL). È possibile utilizzare 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 utilizzare 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. L'assembler MSIL non esegue tali controlli. Tuttavia, se si tenta di caricare un tipo che viola le regole, viene generato un oggetto TypeLoadException.
Per informazioni e per un esempio di codice, vedere Varianza nelle interfacce generiche (C# e Visual Basic).
Torna all'inizio
Elenco di tipi di interfacce e delegati generici varianti
In .NET Framework 4, il tipo di interfaccia e il tipo delegato seguenti presentano parametri di tipo covariante e/o controvariante.
Tipo |
Parametri di tipo covariante |
Parametri di tipo controvariante |
---|---|---|
Action<T> in Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> |
Sì |
|
Sì |
||
Sì |
Sì |
|
Sì |
||
Func<T, TResult> in Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult> |
Sì |
Sì |
Sì |
||
Yes |
||
Sì |
||
Sì |
||
Sì |
||
Sì |
||
Sì |
||
Sì |
||
Sì |
||
Sì |
Torna all'inizio
Vedere anche
Concetti
Varianza nei delegati (C# e Visual Basic)