Đây là một ví dụ đơn giản sử dụng hệ thống phân cấp thừa kế.
Đưa ra hệ thống phân cấp lớp đơn giản:
Và trong mã:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Bất biến (tức là các tham số loại chung * không * được trang trí bằng in
hoặc out
từ khóa)
Dường như, một phương pháp như thế này
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... nên chấp nhận một bộ sưu tập không đồng nhất: (nó thực hiện)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Tuy nhiên, vượt qua một bộ sưu tập của một loại dẫn xuất nhiều hơn thất bại!
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>'
Tại sao? Bởi vì tham số chung IList<LifeForm>
không phải là covariant -
IList<T>
là bất biến, nên IList<LifeForm>
chỉ chấp nhận các bộ sưu tập (thực hiện IList) trong đó loại tham số T
phải là LifeForm
.
Nếu việc triển khai phương thức PrintLifeForms
là độc hại (nhưng có cùng chữ ký phương thức), lý do tại sao trình biên dịch ngăn chặn vượt qua List<Giraffe>
trở nên rõ ràng:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Vì IList
cho phép thêm hoặc loại bỏ các phần tử, LifeForm
do đó , bất kỳ lớp con nào có thể được thêm vào tham số lifeForms
và sẽ vi phạm loại của bất kỳ tập hợp các loại dẫn xuất nào được truyền cho phương thức. (Ở đây, phương pháp độc hại sẽ cố gắng thêm một Zebra
đến var myGiraffes
). May mắn thay, trình biên dịch bảo vệ chúng ta khỏi nguy hiểm này.
Hiệp phương sai (Chung với loại tham số trang trí với out
)
Hiệp phương sai được sử dụng rộng rãi với các bộ sưu tập bất biến (tức là nơi các yếu tố mới không thể được thêm hoặc xóa khỏi bộ sưu tập)
Giải pháp cho ví dụ trên là để đảm bảo rằng loại bộ sưu tập chung covariant được sử dụng, ví dụ IEnumerable
(được định nghĩa là IEnumerable<out T>
). IEnumerable
không có phương thức nào để thay đổi bộ sưu tập và do kết quả của out
hiệp phương sai, mọi bộ sưu tập có kiểu con LifeForm
bây giờ có thể được chuyển sang phương thức:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
bây giờ có thể được gọi với Zebras
, Giraffes
và bất kỳ IEnumerable<>
của bất kỳ lớp con củaLifeForm
Chống chỉ định (Chung với loại tham số được trang trí bằng in
)
Chống chỉ định thường được sử dụng khi các hàm được truyền dưới dạng tham số.
Đây là một ví dụ về hàm, lấy Action<Zebra>
tham số làm tham số và gọi nó trên một thể hiện đã biết của Zebra:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Như mong đợi, điều này hoạt động tốt:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Theo trực giác, điều này sẽ thất bại:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Tuy nhiên, điều này thành công
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
và thậm chí điều này cũng thành công:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Tại sao? Bởi vì Action
được định nghĩa là Action<in T>
, nghĩa là nó contravariant
có nghĩa là Action<Zebra> myAction
, myAction
có thể ở mức "nhất" a Action<Zebra>
, nhưng các siêu lớp có nguồn gốc ít hơn Zebra
cũng được chấp nhận.
Mặc dù lúc đầu điều này có thể không trực quan (ví dụ: làm thế nào có Action<object>
thể truyền như một tham số yêu cầu Action<Zebra>
?), Nếu bạn giải nén các bước, bạn sẽ lưu ý rằng chính hàm được gọi ( PerformZebraAction
) chịu trách nhiệm truyền dữ liệu (trong trường hợp này là một Zebra
trường hợp ) cho chức năng - dữ liệu không đến từ mã gọi.
Do cách tiếp cận ngược của việc sử dụng các hàm bậc cao hơn theo cách này, vào thời điểm Action
được gọi, nó là Zebra
trường hợp dẫn xuất nhiều hơn được gọi với zebraAction
hàm (được truyền dưới dạng tham số), mặc dù chính hàm đó sử dụng loại ít dẫn xuất hơn.