Hier ist ein einfaches Beispiel mit einer Vererbungshierarchie.
Angesichts der einfachen Klassenhierarchie:
Und im Code:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Invarianz (dh generische Typparameter * nicht * verziert mit in
oder out
Schlüsselwörter)
Scheinbar eine Methode wie diese
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... sollte eine heterogene Sammlung akzeptieren: (was es tut)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Das Übergeben einer Sammlung eines abgeleiteten Typs schlägt jedoch fehl!
var myGiraffes = new List<Giraffe>
{
new Giraffe(), // "Jerry"
new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!
cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'
Warum? Da der generische Parameter IList<LifeForm>
nicht kovariant ist - er
IList<T>
ist invariant, IList<LifeForm>
akzeptiert er nur Sammlungen (die IList implementieren), in denen der parametrisierte Typ sein T
muss LifeForm
.
Wenn die Methodenimplementierung von PrintLifeForms
böswillig war (aber dieselbe Methodensignatur hat), wird der Grund, warum der Compiler das Übergeben verhindert, List<Giraffe>
offensichtlich:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Da IList
das Hinzufügen oder Entfernen von Elementen möglich ist, LifeForm
könnte dem Parameter eine beliebige Unterklasse von hinzugefügt werden lifeForms
, die den Typ einer an die Methode übergebenen Sammlung abgeleiteter Typen verletzen würde. (Hier würde die böswillige Methode versuchen, ein Zebra
zu hinzuzufügen var myGiraffes
). Glücklicherweise schützt uns der Compiler vor dieser Gefahr.
Kovarianz (generisch mit parametrisiertem Typ verziert mit out
)
Kovarianz wird häufig bei unveränderlichen Sammlungen verwendet (dh wenn neue Elemente nicht zu einer Sammlung hinzugefügt oder daraus entfernt werden können).
Die Lösung für das obige Beispiel besteht darin, sicherzustellen, dass ein kovarianter generischer Sammlungstyp verwendet wird, z. B. IEnumerable
(definiert als IEnumerable<out T>
). IEnumerable
Es gibt keine Methoden zum Ändern der Sammlung. Aufgrund der out
Kovarianz LifeForm
kann jetzt jede Sammlung mit dem Subtyp von an die Methode übergeben werden:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
kann nun mit genannt werden Zebras
, Giraffes
und jede IEnumerable<>
von jeder Unterklasse vonLifeForm
Kontravarianz (generisch mit parametrisiertem Typ verziert mit in
)
Kontravarianz wird häufig verwendet, wenn Funktionen als Parameter übergeben werden.
Hier ist ein Beispiel für eine Funktion, die a Action<Zebra>
als Parameter verwendet und es für eine bekannte Instanz eines Zebras aufruft:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Wie erwartet funktioniert dies einwandfrei:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Intuitiv wird dies fehlschlagen:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Dies ist jedoch erfolgreich
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
und auch das gelingt:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Warum? Weil Action
definiert ist als Action<in T>
, dh es ist contravariant
, was bedeutet, dass für Action<Zebra> myAction
, das myAction
kann höchstens a sein Action<Zebra>
, aber weniger abgeleitete Oberklassen von Zebra
sind auch akzeptabel.
Obwohl dies zunächst möglicherweise nicht intuitiv ist (z. B. wie kann ein Action<object>
Parameter als Parameter übergeben werden Action<Zebra>
?), Werden Sie beim Entpacken der Schritte feststellen, dass die aufgerufene Funktion ( PerformZebraAction
) selbst für die Übergabe von Daten verantwortlich ist (in diesem Fall eine Zebra
Instanz) ) zur Funktion - die Daten stammen nicht aus dem aufrufenden Code.
Aufgrund des umgekehrten Ansatzes, Funktionen höherer Ordnung auf diese Weise zu verwenden, wird zum Zeitpunkt des Action
Aufrufs der Zebra
Instanz die stärker abgeleitete Instanz für die zebraAction
Funktion aufgerufen (als Parameter übergeben), obwohl die Funktion selbst einen weniger abgeleiteten Typ verwendet.