นี่คือตัวอย่างง่ายๆโดยใช้ลำดับชั้นการสืบทอด
รับลำดับชั้นเรียนที่เรียบง่าย:
และในรหัส:
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>
ไม่ covariant -
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
)
ความแปรปรวนร่วมถูกนำมาใช้อย่างกว้างขวางกับคอลเลกชันที่ไม่เปลี่ยนรูป (เช่นที่องค์ประกอบใหม่ไม่สามารถเพิ่มหรือลบออกจากคอลเลกชัน)
วิธีแก้ไขตัวอย่างข้างต้นคือเพื่อให้แน่ใจว่ามีการใช้ประเภทคอลเลกชันทั่วไป covariant เช่น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
Contravariance (ทั่วไปพร้อมชนิดพารามิเตอร์ที่ตกแต่งด้วย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>
แต่ superclasses ที่ได้รับน้อยกว่าของZebra
ยังเป็นที่ยอมรับ
แม้ว่านี่อาจจะไม่ใช่งานง่ายในตอนแรก (เช่นว่าสามารถAction<object>
ถูกส่งผ่านเป็นพารามิเตอร์ที่กำหนดAction<Zebra>
?) ถ้าคุณแกะขั้นตอนที่คุณจะทราบว่าที่เรียกว่าฟังก์ชั่น ( PerformZebraAction
) ตัวเองเป็นผู้รับผิดชอบสำหรับการส่งผ่านข้อมูล (ในกรณีนี้Zebra
เช่น ) ไปยังฟังก์ชัน - ข้อมูลไม่ได้มาจากรหัสการโทร
เนื่องจากวิธีการกลับด้านของการใช้ฟังก์ชั่นการสั่งซื้อที่สูงขึ้นในลักษณะนี้ตามเวลาที่Action
ถูกเรียกมันเป็นZebra
อินสแตนซ์ที่ได้รับมากขึ้นซึ่งถูกเรียกใช้กับzebraAction
ฟังก์ชั่น (ผ่านเป็นพารามิเตอร์) แม้ว่าฟังก์ชั่นเอง