Ось простий приклад використання ієрархії спадкування.
З огляду на просту ієрархію класів:
І в коді:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Інваріантність (тобто параметри загального типу * не * прикрашені in
або out
ключові слова)
Здається, такий метод, як цей
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... повинен прийняти неоднорідну колекцію: (що вона робить)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Однак передача колекції більш похідного типу не вдається!
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>'
Чому? Оскільки загальний параметр IList<LifeForm>
не є коваріантним -
IList<T>
є інваріантним, тому IList<LifeForm>
приймає лише колекції (які реалізують IList) там, де T
повинен бути параметризований тип LifeForm
.
Якщо реалізація методу PrintLifeForms
була шкідливою (але має такий же підпис методу), причина, через яку компілятор перешкоджає передачі, List<Giraffe>
стає очевидною:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Оскільки IList
дозволяє додавати або видаляти елементи, будь-який підклас з LifeForm
цього приводу може бути доданий до параметра lifeForms
і порушує тип будь-якої колекції похідних типів, переданих методу. (Тут шкідливий метод намагатиметься додати Zebra
до var myGiraffes
). На щастя, компілятор захищає нас від цієї небезпеки.
Коваріація (загальна з параметризованим типом, прикрашена out
)
Коваріація широко використовується для незмінних колекцій (тобто там, де нові елементи не можуть бути додані або вилучені з колекції)
Рішенням прикладу вище є забезпечення коваріантного загального типу колекції, наприклад IEnumerable
(визначеного як IEnumerable<out T>
). IEnumerable
не має методів зміни до колекції, і в результаті out
коваріації тепер будь-яка колекція з підтипом LifeForm
може бути передана методу:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
тепер може бути викликаний Zebras
, Giraffes
і будь-який IEnumerable<>
з будь-якого підкласуLifeForm
Протиріччя (загальне із типом, параметризованим типом, прикрашене in
)
Протилежність часто використовується, коли функції передаються як параметри.
Ось приклад функції, яка приймає Action<Zebra>
як параметр і викликає його у відомій екземплярі Zebra:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Як і очікувалося, це працює чудово:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Інтуїтивно це не вдасться:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Однак це вдається
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
і навіть це також вдається:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Чому? Тому що Action
визначається як Action<in T>
, тобто це contravariant
, означає, що для Action<Zebra> myAction
, що myAction
може бути якнайбільше "a" Action<Zebra>
, але менш похідні суперкласи Zebra
також є прийнятними.
Хоча це може бути спочатку не інтуїтивно зрозумілим (наприклад, як можна Action<object>
передавати його як параметр, що вимагає Action<Zebra>
?), Якщо ви розпакуєте етапи, ви зауважите, що сама викликана функція ( PerformZebraAction
) відповідає за передачу даних (у цьому випадку Zebra
екземпляр ) до функції - дані не надходять з коду виклику.
Через інвертований підхід використання функцій вищого порядку таким чином, до моменту Action
виклику це більш похідний Zebra
екземпляр викликається zebraAction
функцією (передається як параметр), хоча сама функція використовує менш похідний тип.