2010-02-02 34 views

Trả lời

234

Câu hỏi đặt ra là "sự khác biệt giữa hiệp phương sai và contravariance là gì?"

Hiệp phương sai và đối xứng là các thuộc tính của chức năng ánh xạ liên kết một thành viên của tập hợp với khác. Cụ thể hơn, một ánh xạ có thể là biến thể hoặc contravariant đối với mối quan hệ trên bộ đó.

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, điều này thiết lập liên quan rõ ràng:

{ IEnumerable<Animal>, 
    IEnumerable<Tiger>, 
    IEnumerable<Fruit>, 
    IEnumerable<Banana> } 

Có một mapping hoạt động từ tập đầu tiên đến tập thứ hai. Tức là, đối 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ũi tên mỏng".

Với tôi cho đến nay?

Bây giờ, hãy xem xét mối quan hệ . Có mối quan hệ tương thích với nhiệm vụ gán giữa các cặp trong tập đầu tiên. Giá trị loại Tiger có thể được gán cho một biến loại Animal, vì vậy các loại này được cho là "tương thích gán". Hãy viết "giá trị 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ũ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 công việc:

Tiger ⇒ Tiger 
Tiger ⇒ Animal 
Animal ⇒ Animal 
Banana ⇒ Banana 
Banana ⇒ Fruit 
Fruit ⇒ Fruit 

Trong C# 4, hỗ trợ khả năng tương thích phân hiệp biến của giao diện nào đó, có một mối quan hệ phù hợp nhiệm vụ giữa các cặp loại trong tập 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> 

Chú ý rằng các bản đồ T → IE<T>bảo sự tồn tại và hướng nhiệm vụ tương thích. Tức là, nếu X ⇒ Y, thì cũng đúng là IE<X> ⇒ IE<Y>.

Nếu chúng ta có hai thứ ở hai bên của mũi tên chất béo, thì chúng ta có thể thay thế cả hai mặt bằng thứ gì đó ở phía bên phải của mũi tên mỏng tương ứng.

Á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 các con hổ có thể được sử dụng khi một chuỗi các loài động vật là cần thiết, nhưng ngược lại là không đúng sự thật. Một chuỗi các loài động vật không nhất thiết phải được sử dụng khi một chuỗi các con hổ là cần thiết.

Đó là hiệp phương sai. Bây giờ hãy xem xét tậ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à, các bản đồ T → IC<T> đã bảo tồn sự tồn tại nhưng đảo ngược hướng tương thích chuyển nhượng. Tức là, nếu X ⇒ Y, thì IC<X> ⇐ IC<Y>.

Ánh xạ duy trì nhưng đảo ngược một mối quan hệ được gọi là ánh xạ contravariant.

Một lần nữa, điều này phải rõ ràng chính xác. Một thiết bị có thể so sánh hai con vật cũng có thể so sánh hai con hổ, nhưng một thiết bị có thể so sánh hai con hổ không nhất thiết phải so sánh hai con vật.

Vì vậy, đó là sự khác biệt giữa hiệp phương sai và contravariance trong C# 4. Hiệp phương sai bảo tồn hướng chuyển nhượng. Contravariance đảo ngược.

+2

Đối với một người như tôi, sẽ tốt hơn nếu thêm các ví dụ cho thấy cái gì không phải là biến thể và cái gì là KHÔNG contravariant và cái gì KHÔNG là cả hai. – bjan

+0

Cảm ơn Eric, có vẻ giống như java của java. – Bargitta

+0

@Bargitta: Nó rất giống nhau. Sự khác biệt là C# sử dụng * phương sai xác định trang web * và Java sử dụng * phương sai trang web cuộc gọi *. Vì vậy, cách mọi thứ khác nhau là như nhau, nhưng nơi mà các nhà phát triển nói "Tôi cần điều này là biến thể" là khác nhau. Ngẫu nhiên, tính năng trong cả hai ngôn ngữ là một phần được thiết kế bởi cùng một người! –

94

Có lẽ dễ nhất là đưa ra các ví dụ - đó chắc chắn là cách tôi nhớ chúng.

Hiệp phương sai

ví dụ Canonical: IEnumerable<out T>, Func<out T>

Bạn có thể chuyển đổi từ IEnumerable<string> để IEnumerable<object>, hoặc Func<string>-Func<object>. Giá trị chỉ đến trong số các đối tượng này.

Nó hoạt động vì nếu bạn chỉ lấy giá trị API, và nó sẽ trả về một cái gì đó cụ thể (như string), bạn có thể coi giá trị trả về đó là một loại tổng quát hơn (như object).

Contravariance

ví dụ Canonical: IComparer<in T>, Action<in T>

Bạn có thể chuyển đổi từ IComparer<object> để IComparer<string>, hoặc Action<object> để Action<string>; giá trị chỉ đi thành 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ể được hiệp biến trong T (tức là khai báo nó như IFoo<out T> nếu T chỉ được sử dụng ở một vị trí đầu ra (ví dụ như một kiểu trả về) trong giao diện. Nó . có thể được contravariant trong T (tức IFoo<in T>) nếu T chỉ được sử dụng ở một vị trí đầu vào (ví dụ như một loại tham số)

nó được khả năng gây nhầm lẫn vì "vị trí đầu ra" không phải là khá đơn giản như nó âm thanh - một tham số loại Action<T> vẫn chỉ sử dụng T ở vị trí đầu ra - contravariance của Action<T> quay vòng, nếu bạn hiểu ý tôi. Đó là một "đầu ra" trong đó các giá trị có thể chuyển từ việc thực hiện phương thức về phía mã số của người gọi, giống như một giá trị trả về có thể là. Thông thường loại điều này không xuất hiện, may mắn thay :)

+1

Đối với một người như tôi, sẽ tốt hơn nếu thêm ví dụ cho thấy những gì không phải là biến thể và những gì KHÔNG tương phản và những gì KHÔNG phải là cả hai. – bjan

+1

@Jon Skeet Ví dụ điển hình, tôi chỉ không hiểu * "một tham số kiểu' Hành động 'vẫn chỉ sử dụng' T' ở vị trí đầu ra "*. 'Loại hành động ' trả về là vô hiệu, làm sao nó có thể sử dụng 'T' làm đầu ra? Hay điều đó có nghĩa là, 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? –

+1

Với bản thân tương lai của tôi, người sẽ quay lại câu trả lời tuyệt vời này ** một lần nữa ** để học lại sự khác biệt, đây là dòng bạn muốn: _ "[Hiệp phương sai] hoạt động bởi vì nếu bạn chỉ lấy giá trị ngoài API, và nó sẽ trả lại một cái gì đó cụ thể (như chuỗi), bạn có thể coi giá trị trả về đó là một kiểu tổng quát hơn (như đối tượng). "_ –

13

Tôi hy vọng bài đăng của mình sẽ giúp bạn có được chế độ xem thuyết bất khả tri về ngôn ngữ của chủ đề.

Đối với các khóa đào tạo nội bộ của mình, tôi đã làm việc với cuốn sách tuyệt vời "Smalltalk, Objects and Design (Chamond Liu)" và tôi lặp lại các ví dụ sau.

"Tính nhất quán" có nghĩa là gì? Ý tưởng là thiết kế phân cấp loại an toàn loại 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 bằng một ngôn ngữ được gõ tĩnh. (Chúng tôi sẽ thảo luận Liskov Substitution Nguyên tắc (LSP) trên một mức độ cao ở đây.)

Ví dụ thực tế (mã giả/không hợp lệ trong C#):

  • Hiệp phương sai: Giả sử Birds mà đẻ trứng “ một cách nhất quán ”với kiểu gõ tĩnh: Nếu kiểu Bird đẻ trứng, thì kiểu phụ của Bird có phải là một kiểu con của Egg không? Ví dụ. loại vịt đẻ một DuckEgg, sau đó sự 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ế hợp pháp bởi một Bird hoặc bởi một cá thể vịt. Chúng ta nói kiểu trả về là covariant cho kiểu, trong đó Lay() được định nghĩa. Ghi đè của một loại phụ có thể trả về loại chuyên biệt hơn. => “Họ phân phối nhiều hơn”.

  • Nghịch lý: Giả sử Pianos rằng nghệ sĩ piano có thể chơi “nhất quán” với kiểu gõ tĩnh: Nếu một nghệ sĩ dương cầm chơi Piano, cô ấy có thể chơi GrandPiano không? Sẽ không phải là một Virtuoso chơi một GrandPiano? (Được cảnh báo, có một twist!) Điều này là không phù hợp! Bởi vì trong một biểu hiện như vậy: aPiano.Play(aPianist); aPiano không thể được thay thế hợp pháp bằng Piano hoặc bởi một phiên bản GrandPiano! Một GrandPiano chỉ có thể được chơi bởi một Virtuoso, nghệ sĩ dương cầm là quá chung chung! GrandPianos phải có thể chơi được bằng các loại tổng quát hơn, sau đó chơi là nhất quán. Chúng ta nói kiểu tham số là contravariant với kiểu, trong đó Play() được định nghĩa. Ghi đè của một loại phụ có thể chấp nhận loại tổng quát hơn. => “Họ đòi hỏi ít hơn.”

Back to C#:
Bởi vì C# cơ bản là một ngôn ngữ tĩnh đánh máy, các "địa điểm" giao diện của một loại mà nên đồng hoặc contravariant (ví dụ như thông số và lợi nhuận loại), phải được đánh dấu một cách rõ ràng để đảm bảo việc sử dụng/phát triển phù hợp loại đó, để làm cho LSP hoạt động tốt. Trong ngôn ngữ tự động gõ LSP nhất quán thường không phải là một vấn đề, nói cách khác bạn hoàn toàn có thể loại bỏ "đánh dấu" đồng thời và contravariant trên giao diện .Net và các đại biểu, nếu bạn chỉ sử dụng kiểu động trong các kiểu của bạn. - 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 năng động trong 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 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 tính thống nhất khác, đặc biệt. cũng có các loại tham số covariant, bởi vì nó mô tả tốt hơn thực tế hơn lý tưởng lý thuyết. Trong các ngôn ngữ được nhập tĩnh, độ nhất quán mong muốn phải đạt được bằng cách áp dụng các mẫu thiết kế như “cử đôi” và “khách truy cập”. Các ngôn ngữ khác cung cấp cái gọi là "nhiều công văn" hoặc đa phương pháp (về cơ bản là chọn quá tải chức năng tại thời gian chạy, ví dụ: với CLOS) hoặc nhận hiệu ứng mong muốn bằng cách sử dụng tính năng nhập động.

+0

Bạn nói * Ghi đè của một subtype có thể trở lại một loại chuyên biệt hơn *. Nhưng điều đó hoàn toàn không đúng sự thật. Nếu 'Bird' định nghĩa' public BirdEgg Lay(); ', sau đó' Duck: Bird' * PHẢI * thực hiện 'public override BirdEgg Lay() {}' Vì vậy, xác nhận của bạn rằng 'BirdEgg anEgg = aBird.Lay();' có bất kỳ loại phương sai nào cả đều đơn giản là không đúng sự thật. Là tiền đề của điểm giải thích, toàn bộ vấn đề đã biến mất. Bạn sẽ * thay vào đó * nói rằng hiệp phương sai tồn tại trong quá trình thực hiện, nơi một DuckEgg được ngầm sử dụng để loại ra/trả về BirdEgg? Dù bằng cách nào, xin vui lòng rõ ràng sự nhầm lẫn của tôi. – Suamere

+1

Để cắt ngắn: bạn đã đúng! Xin lỗi vì sự nhầm lẫn. 'DuckEgg Lay()' không phải là ghi đè hợp lệ cho 'Egg Lay()' _in C# _, và đó là điểm mấu chốt. C# không hỗ trợ các kiểu trả về biến đổi, nhưng Java cũng như C++ làm. Tôi thay vì mô tả lý tưởng lý thuyết bằng cách sử dụng cú pháp C#. Trong C# bạn cần cho phép Bird và Duck thực hiện một giao diện chung, trong đó Lay được định nghĩa là có một sự trở về biến đổi (nghĩa là kiểu out-specification), sau đó các vấn đề phù hợp với nhau! – Nico

2

Nếu bạn muốn gán bất kỳ phương thức nào cho người được ủy quyền, chữ ký phương thức phải khớp chính xác với chữ ký của người được ủy quyền. Có nói rằng, hiệp phương sai và contravariance cho phép một số mức độ linh hoạt trong phù hợp với chữ ký của các phương pháp để của đại biểu.

Bạn có thể tham khảo điều này article to understand covariance, contravariance, and the differences between them.

1

Đạ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 hiệp phương sai nơi một phương thức trả về một loại 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(); 
Các vấn đề liên quan