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 inoder outSchlü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 Tmuss LifeForm.
Wenn die Methodenimplementierung von PrintLifeFormsbö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 IListdas Hinzufügen oder Entfernen von Elementen möglich ist, LifeFormkö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 Zebrazu 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>). IEnumerableEs gibt keine Methoden zum Ändern der Sammlung. Aufgrund der outKovarianz LifeFormkann 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());
}
}
PrintLifeFormskann nun mit genannt werden Zebras, Giraffesund 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 Actiondefiniert ist als Action<in T>, dh es ist contravariant, was bedeutet, dass für Action<Zebra> myAction, das myActionkann höchstens a sein Action<Zebra>, aber weniger abgeleitete Oberklassen von Zebrasind 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 ZebraInstanz) ) 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 ActionAufrufs der ZebraInstanz die stärker abgeleitete Instanz für die zebraActionFunktion aufgerufen (als Parameter übergeben), obwohl die Funktion selbst einen weniger abgeleiteten Typ verwendet.