Câu trả lời:
Câu hỏi là "sự khác biệt giữa hiệp phương sai và chống chỉ định là gì?"
Hiệp phương sai và chống chỉ định là các thuộc tính của hàm ánh xạ liên kết một thành viên của tập hợp với tập hợp khác . Cụ thể hơn, một ánh xạ có thể là covariant hoặc contravariant đối với mối quan hệ trên tập hợp đó.
Hãy xem xét hai tập hợp con sau của tập hợp tất cả các loại C #. Đầu tiên:
{ Animal,
Tiger,
Fruit,
Banana }.
Và thứ hai, bộ này liên quan rõ ràng:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
Có một hoạt động ánh xạ từ bộ đầu tiên đến bộ thứ hai. Nghĩa là, với mỗi T trong tập đầu tiên, loại tương ứng trong tập thứ hai là IEnumerable<T>
. Hoặc, ở dạng ngắn, ánh xạ là T → IE<T>
. Lưu ý rằng đây là một "mũi tên mỏng".
Với tôi cho đến nay?
Bây giờ hãy xem xét một mối quan hệ . Có một mối quan hệ tương thích gán giữa các cặp loại trong tập đầu tiên. Một giá trị của loại Tiger
có thể được gán cho một biến loại Animal
, vì vậy những loại này được gọi là "tương thích gán". Chúng ta hãy viết "một giá trị của loại X
có thể được gán cho một biến loại Y
" ở dạng ngắn hơn : X ⇒ Y
. Lưu ý rằng đây là một "mũi tên béo".
Vì vậy, trong tập hợp con đầu tiên của chúng tôi, đây là tất cả các mối quan hệ tương thích gán:
Tiger ⇒ Tiger
Tiger ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit ⇒ Fruit
Trong C # 4, hỗ trợ khả năng tương thích gán covariant của một số giao diện nhất định, có mối quan hệ tương thích gán giữa các cặp loại trong bộ thứ hai:
IE<Tiger> ⇒ IE<Tiger>
IE<Tiger> ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit> ⇒ IE<Fruit>
Lưu ý rằng ánh xạ T → IE<T>
bảo tồn sự tồn tại và hướng tương thích gán . Đó là, nếu X ⇒ Y
, thì nó cũng đúng đó IE<X> ⇒ IE<Y>
.
Nếu chúng ta có hai thứ ở hai bên của một mũi tên béo, thì chúng ta có thể thay thế cả hai bên bằng một thứ gì đó ở phía bên phải của một mũi tên mỏng tương ứng.
Một ánh xạ có thuộc tính này liên quan đến một mối quan hệ cụ thể được gọi là "ánh xạ covariant". Điều này sẽ có ý nghĩa: một chuỗi Hổ có thể được sử dụng khi cần một chuỗi Động vật, nhưng điều ngược lại là không đúng. Một chuỗi các động vật không nhất thiết phải được sử dụng khi cần một chuỗi Hổ.
Đó là hiệp phương sai. Bây giờ hãy xem xét tập hợp con này của tập hợp tất cả các loại:
{ IComparable<Tiger>,
IComparable<Animal>,
IComparable<Fruit>,
IComparable<Banana> }
bây giờ chúng ta có ánh xạ từ tập đầu tiên đến tập thứ ba T → IC<T>
.
Trong C # 4:
IC<Tiger> ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger> Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit> ⇒ IC<Banana> Backwards!
IC<Fruit> ⇒ IC<Fruit>
Đó là, ánh xạ T → IC<T>
đã bảo tồn sự tồn tại nhưng đảo ngược hướng tương thích gán. Đó là, nếu X ⇒ Y
, sau đó IC<X> ⇐ IC<Y>
.
Một bản đồ mà bảo tồn nhưng đảo ngược một mối quan hệ được gọi là contravariant lập bản đồ.
Một lần nữa, điều này nên được chính xác rõ ràng. Một thiết bị có thể so sánh hai Động vật cũng có thể so sánh hai Hổ, nhưng một thiết bị có thể so sánh hai Hổ không nhất thiết phải so sánh bất kỳ hai Động vật nào.
Vì vậy, đó là sự khác biệt giữa hiệp phương sai và chống chỉ định trong C # 4. Hiệp phương sai bảo toàn hướng chuyển nhượng. Chống chỉ định đảo ngược nó.
IEnumerable<Tiger>
để IEnumerable<Animal>
an toàn? Bởi vì không có cách nào để nhập một con hươu cao cổ vào IEnumerable<Animal>
. Tại sao chúng ta có thể chuyển đổi một IComparable<Animal>
đến IComparable<Tiger>
? Bởi vì không có cách nào để lấy ra một con hươu cao cổ từ một IComparable<Animal>
. Có lý?
Có lẽ dễ nhất để đưa ra ví dụ - đó chắc chắn là cách tôi nhớ chúng.
Hiệp phương sai
Ví dụ điển hình : IEnumerable<out T>
,Func<out T>
Bạn có thể chuyển đổi từ IEnumerable<string>
sang IEnumerable<object>
, hoặc Func<string>
sang Func<object>
. Giá trị chỉ đi ra từ những đối tượng này.
Nó hoạt động vì nếu bạn chỉ lấy các giá trị ra khỏi API và nó sẽ trả về một cái gì đó cụ thể (như string
), bạn có thể coi giá trị được trả về đó là một loại tổng quát hơn (như object
).
Chống chỉ định
Ví dụ điển hình : IComparer<in T>
,Action<in T>
Bạn có thể chuyển đổi từ IComparer<object>
sang IComparer<string>
, hoặc Action<object>
sang Action<string>
; các giá trị chỉ đi vào các đối tượng này.
Lần này nó hoạt động vì nếu API đang mong đợi một cái gì đó chung chung (như object
), bạn có thể cung cấp cho nó một cái gì đó cụ thể hơn (như string
).
Tổng quát hơn
Nếu bạn có một giao diện, IFoo<T>
nó có thể là covariant T
(nghĩa là khai báo nó như IFoo<out T>
thể T
chỉ được sử dụng ở vị trí đầu ra (ví dụ như kiểu trả về) trong giao diện. Nó có thể được chống lại T
(ví dụ IFoo<in T>
) nếu T
chỉ được sử dụng ở vị trí đầu vào ( ví dụ một loại tham số).
Nó có khả năng gây nhầm lẫn bởi vì "vị trí đầu ra" không đơn giản như âm thanh - một tham số của loại Action<T>
vẫn chỉ được sử dụng T
ở vị trí đầu ra - chống Action<T>
chỉ định xoay tròn, nếu bạn hiểu ý tôi là gì. Đó là một "đầu ra" ở chỗ các giá trị có thể chuyển từ việc triển khai phương thức sang mã của trình gọi, giống như giá trị trả về có thể. Thông thường loại này không xuất hiện, may mắn thay :)
Action<T>
vẫn chỉ sử dụng T
ở vị trí đầu ra" . Action<T>
Kiểu trả về là void, làm thế nào nó có thể sử dụng T
làm đầu ra? Hoặc đó là những gì nó có nghĩa, bởi vì nó không trả lại bất cứ điều gì bạn có thể thấy rằng nó không bao giờ có thể vi phạm quy tắc?
Tôi hy vọng bài viết của tôi sẽ giúp có được cái nhìn bất khả tri về ngôn ngữ của chủ đề này.
Đối với các khóa đào tạo nội bộ của chúng tôi, tôi đã làm việc với cuốn sách tuyệt vời "Smalltalk, Object and Design (Chamond Liu)" và tôi đã đọc lại các ví dụ sau đây.
Không nhất quán có nghĩa là gì? Ý tưởng là thiết kế hệ thống phân cấp loại an toàn với các loại có thể thay thế cao. Chìa khóa để có được tính nhất quán này là sự phù hợp dựa trên loại phụ, nếu bạn làm việc trong một ngôn ngữ được gõ tĩnh. (Chúng ta sẽ thảo luận về Nguyên tắc thay thế Liskov (LSP) ở cấp độ cao ở đây.)
Ví dụ thực tế (mã giả / không hợp lệ trong C #):
Hiệp phương sai: Chúng ta hãy giả sử những con chim đẻ trứng liên tục với kiểu gõ tĩnh: Nếu kiểu Chim đẻ trứng, thì kiểu con của Chim có tạo ra một kiểu con của Trứng không? Ví dụ, kiểu Vịt đặt một DuckEgg, sau đó tính nhất quán được đưa ra. Tại sao điều này phù hợp? Bởi vì trong một biểu thức như vậy: Egg anEgg = aBird.Lay();
tham chiếu aBird có thể được thay thế một cách hợp pháp bởi một con Chim hoặc bởi một cá thể Vịt. Chúng ta nói kiểu trả về là covariant với kiểu, trong đó Lay () được định nghĩa. Ghi đè của một kiểu con có thể trả về một loại chuyên dụng hơn. => Quảng cáo Họ cung cấp nhiều hơn.
Chống chỉ định: Chúng ta hãy giả sử Pianos rằng Pianist có thể chơi liên tục với chế độ gõ tĩnh: Nếu một nghệ sĩ piano chơi Piano, liệu cô ấy có thể chơi GrandPiano không? Không phải là một Virtuoso chơi GrandPiano? (Được cảnh báo; có một thay đổi!) Điều này không nhất quán! Bởi vì trong một biểu hiện như vậy: aPiano.Play(aPianist);
aPiano không thể được thay thế một cách hợp pháp bởi Piano hoặc bởi một ví dụ GrandPiano! Một GrandPiano chỉ có thể được chơi bởi một Virtuoso, Pianist quá chung chung! GrandPianos phải được chơi bởi các loại chung hơn, sau đó chơi là nhất quán. Chúng ta nói loại tham số là tương phản với loại, trong đó Play () được xác định. Ghi đè của một kiểu con có thể chấp nhận một loại tổng quát hơn. => Quảng cáo Họ yêu cầu ít hơn.
Quay lại C #:
Vì C # về cơ bản là ngôn ngữ được nhập tĩnh, nên "vị trí" của giao diện của một loại phải là co-hoặc contravariant (ví dụ: tham số và loại trả về), phải được đánh dấu rõ ràng để đảm bảo việc sử dụng / phát triển nhất quán của loại đó , để làm cho LSP hoạt động tốt. Trong các ngôn ngữ được gõ động, tính nhất quán LSP thường không phải là vấn đề, nói cách khác, bạn hoàn toàn có thể thoát khỏi "đánh dấu" đồng thời và chống chỉ định trên các giao diện và đại biểu .Net, nếu bạn chỉ sử dụng kiểu động trong các loại của mình. - Nhưng đây không phải là giải pháp tốt nhất trong C # (bạn không nên sử dụng động trong các giao diện công cộng).
Quay lại lý thuyết:
Sự phù hợp được mô tả (loại trả về covariant / loại tham số contravariant) là lý tưởng lý thuyết (được hỗ trợ bởi các ngôn ngữ Emerald và POOL-1). Một số ngôn ngữ oop (ví dụ Eiffel) đã quyết định áp dụng một loại thống nhất khác, đặc biệt. cũng các loại tham số covariant, bởi vì nó mô tả thực tế tốt hơn lý tưởng lý thuyết. Trong các ngôn ngữ được nhập tĩnh, tính nhất quán mong muốn thường phải đạt được bằng cách áp dụng các mẫu thiết kế như cách thức tăng gấp đôi và lượt khách truy cập. Các ngôn ngữ khác cung cấp cái gọi là đa phương thức, nhiều phương thức, hay phương thức đa phương thức (điều này về cơ bản là chọn quá tải hàm khi chạy , ví dụ như với CLOS) hoặc có được hiệu ứng mong muốn bằng cách sử dụng kiểu gõ động.
Bird
định nghĩa public abstract BirdEgg Lay();
, thì Duck : Bird
PHẢI thực hiện public override BirdEgg Lay(){}
Vì vậy, khẳng định của bạn BirdEgg anEgg = aBird.Lay();
có bất kỳ loại phương sai nào là không đúng. Là tiền đề của điểm giải thích, toàn bộ điểm này đã biến mất. Thay vào đó, bạn có muốn nói rằng hiệp phương sai tồn tại trong quá trình triển khai khi DuckEgg được ngầm định chuyển sang loại trả / trả lại BirdEgg không? Dù bằng cách nào, xin vui lòng xóa sự nhầm lẫn của tôi.
DuckEgg Lay()
không phải là ghi đè hợp lệ cho Egg Lay()
C # và đó là mấu chốt. C # không hỗ trợ các kiểu trả về covariant, nhưng Java cũng như C ++ thì có. Tôi thay vì mô tả lý tưởng lý thuyết bằng cách sử dụng cú pháp giống như C #. Trong C #, bạn cần để Bird và Duck thực hiện một giao diện chung, trong đó Lay được xác định là có kiểu trả về covariant (nghĩa là đặc tả ngoài), sau đó các vấn đề khớp với nhau!
extends
, Người tiêu dùng super
".
Các đại biểu chuyển đổi giúp tôi hiểu sự khác biệt.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
đại diện cho hiệp phương sai trong đó một phương thức trả về một kiểu cụ thể hơn .
TInput
đại diện contravariance nơi một phương pháp được thông qua một loại ít cụ thể hơn .
public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }
public static Poodle ConvertDogToPoodle(Dog dog)
{
return new Poodle() { Name = dog.Name };
}
List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
Co và Contra variance là những thứ khá logic. Hệ thống loại ngôn ngữ buộc chúng ta phải hỗ trợ logic cuộc sống thực. Thật dễ hiểu bằng ví dụ.
Chẳng hạn, bạn muốn mua một bông hoa và bạn có hai cửa hàng hoa trong thành phố của mình: cửa hàng hoa hồng và cửa hàng hoa cúc.
Nếu bạn hỏi ai đó "cửa hàng hoa ở đâu?" và ai đó nói cho bạn biết cửa hàng hoa hồng ở đâu, có ổn không? Vâng, bởi vì hoa hồng là một bông hoa, nếu bạn muốn mua một bông hoa bạn có thể mua một bông hồng. Điều tương tự cũng áp dụng nếu ai đó trả lời bạn bằng địa chỉ của cửa hàng cúc.
Đây là ví dụ về hiệp phương sai : bạn được phép truyền A<C>
tới A<B>
, trong đó C
một lớp con của B
, nếu A
tạo ra các giá trị chung (trả về như là kết quả từ hàm). Hiệp phương sai là về các nhà sản xuất, đó là lý do tại sao C # sử dụng từ khóa out
cho hiệp phương sai.
Các loại:
class Flower { }
class Rose: Flower { }
class Daisy: Flower { }
interface FlowerShop<out T> where T: Flower {
T getFlower();
}
class RoseShop: FlowerShop<Rose> {
public Rose getFlower() {
return new Rose();
}
}
class DaisyShop: FlowerShop<Daisy> {
public Daisy getFlower() {
return new Daisy();
}
}
Câu hỏi là "cửa hàng hoa ở đâu?", Câu trả lời là "cửa hàng hoa hồng ở đó":
static FlowerShop<Flower> tellMeShopAddress() {
return new RoseShop();
}
Chẳng hạn, bạn muốn tặng một bông hoa cho bạn gái của bạn và cô gái của bạn thích bất kỳ bông hoa nào. Bạn có thể coi cô ấy là một người yêu hoa hồng, hay là một người yêu hoa cúc? Vâng, bởi vì nếu cô ấy yêu bất kỳ loài hoa nào, cô ấy sẽ yêu cả hoa hồng và hoa cúc.
Đây là một ví dụ về contravariance : bạn được phép dàn diễn viên A<B>
đến A<C>
, nơi C
là lớp con của B
, nếu A
tiêu thụ giá trị chung. Chống chỉ định là về người tiêu dùng, đó là lý do tại sao C # sử dụng từ khóa in
để chống chỉ định.
Các loại:
interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
void takeGift(TFavoriteFlower flower);
}
class AnyFlowerLover: PrettyGirl<Flower> {
public void takeGift(Flower flower) {
Console.WriteLine("I like all flowers!");
}
}
Bạn đang xem bạn gái của mình yêu bất kỳ loài hoa nào như một người yêu hoa hồng và tặng cô ấy một bông hồng:
PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());