Berikut adalah contoh sederhana menggunakan hierarki warisan.
Dengan hierarki kelas yang sederhana:
Dan dalam kode:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Invarian (mis. Parameter tipe umum * tidak * dihiasi in
atau out
kata kunci)
Tampaknya, metode seperti ini
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... harus menerima koleksi yang heterogen: (yang memang ada)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Namun, melewati koleksi tipe yang lebih diturunkan gagal!
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>'
Mengapa? Karena parameter generikIList<LifeForm>
bukan kovarian -
IList<T>
invarian, jadi IList<LifeForm>
hanya menerima koleksi (yang mengimplementasikan IList) di mana tipe parameternya T
harus LifeForm
.
Jika metode implementasi PrintLifeForms
berbahaya (tetapi memiliki tanda tangan metode yang sama), alasan mengapa kompiler mencegah lewat List<Giraffe>
menjadi jelas:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Karena IList
mengizinkan penambahan atau penghapusan elemen, maka setiap subkelas LifeForm
dapat ditambahkan ke parameterlifeForms
, dan akan melanggar tipe kumpulan dari setiap tipe turunan yang diteruskan ke metode. (Di sini, metode jahat akan berusaha menambahkan Zebra
ke var myGiraffes
). Untungnya, kompiler melindungi kita dari bahaya ini.
Kovarian (Generik dengan tipe parameterisasi yang didekorasi out
)
Kovarian secara luas digunakan dengan koleksi yang tidak dapat diubah (yaitu ketika elemen baru tidak dapat ditambahkan atau dihapus dari koleksi)
Solusi untuk contoh di atas adalah untuk memastikan bahwa tipe pengumpulan generik kovarian digunakan, misalnya IEnumerable
(didefinisikan sebagai IEnumerable<out T>
). IEnumerable
tidak memiliki metode untuk mengubah ke koleksi, dan sebagai hasil dari out
kovarian, koleksi dengan subtipe LifeForm
sekarang dapat diteruskan ke metode:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
sekarang dapat dipanggil dengan Zebras
, Giraffes
dan IEnumerable<>
sembarang subkelas dariLifeForm
Contravariance (Generic dengan tipe parameterized yang didekorasi dengan in
)
Kontravarians sering digunakan ketika fungsi dilewatkan sebagai parameter.
Berikut adalah contoh fungsi, yang menggunakan Action<Zebra>
parameter, dan menjalankannya pada turunan Zebra yang dikenal:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Seperti yang diharapkan, ini berfungsi dengan baik:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Secara intuitif, ini akan gagal:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Namun, ini berhasil
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
dan bahkan ini juga berhasil:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Mengapa? Karena Action
didefinisikan sebagai Action<in T>
, yaitu contravariant
, artinya untuk Action<Zebra> myAction
, yang myAction
dapat berupa "sebagian besar" Action<Zebra>
, tetapi superclasses yang kurang turunan dariZebra
juga dapat diterima.
Meskipun ini mungkin tidak intuitif pada awalnya (misalnya bagaimana bisa Action<object>
dilewatkan sebagai parameter yang memerlukan Action<Zebra>
?), Jika Anda membongkar langkah-langkahnya, Anda akan mencatat bahwa fungsi yang dipanggil ( PerformZebraAction
) itu sendiri bertanggung jawab untuk mengirimkan data (dalam hal ini Zebra
contoh ) ke fungsi - data tidak berasal dari kode panggilan.
Karena pendekatan terbalik menggunakan fungsi urutan yang lebih tinggi dengan cara ini, pada saat Action
dipanggil, itu adalah turunan yang lebih banyak Zebra
yang dipanggil terhadap zebraAction
fungsi (dilewatkan sebagai parameter), meskipun fungsi itu sendiri menggunakan tipe yang kurang diturunkan.