Sự khác biệt giữa hiệp phương sai và phương sai


Câu trả lời:


266

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 Tigercó 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 Xcó 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ó.


4
Đố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 những gì KHÔNG phải là biến đổi và những gì KHÔNG chống chỉ định và những gì KHÔNG phải là cả hai.
bjan

2
@Bargitta: Nó rất giống nhau. Sự khác biệt là C # sử dụng phương sai trang web xác định 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 nhà phát triển nói "Tôi cần biến thể này" thì khác. 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!
Eric Lippert

2
@AshishNegi: Đọc mũi tên là "có thể được sử dụng là". "Một thứ có thể so sánh động vật có thể được sử dụng như một thứ có thể so sánh hổ". Có ý nghĩa bây giờ?
Eric Lippert

1
@AshishNegi: Không, điều đó không đúng. IEnumerable là covariant vì T chỉ xuất hiện trong phần trả về của các phương thức của IEnumerable. Và IComparable là chống chỉ định vì T chỉ xuất hiện dưới dạng tham số chính thức của các phương thức của IComparable .
Eric Lippert

2
@AshishNegi: Bạn muốn nghĩ về những lý do hợp lý mà dưới những mối quan hệ này. Tại sao chúng ta có thể chuyển đổi 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ý?
Eric Lippert

111

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ể Tchỉ đượ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 Tchỉ đượ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 :)


1
Đố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 những gì KHÔNG phải là biến đổi và những gì KHÔNG chống chỉ định và những gì KHÔNG phải là cả hai.
bjan

1
@Jon Skeet Ví dụ hay, tôi chỉ không hiểu "một tham số kiểu 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 Tlà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?
Alexander Derck

2
Đối với 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 để tìm hiểu sự khác biệt, đây là dòng bạn muốn: "[Hiệp phương sai] hoạt động vì nếu bạn chỉ lấy các giá trị ra khỏ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ị được trả về đó là loại tổng quát hơn (như đối tượng). "
Matt Klein

Phần khó hiểu nhất của tất cả những điều này là vì hiệp phương sai hoặc chống chỉ định, nếu bạn bỏ qua hướng (vào hoặc ra), bạn sẽ nhận được thêm cụ thể để chuyển đổi chung hơn nữa! Ý tôi là: "bạn có thể coi rằng giá trị trả về là một kiểu tổng quát hơn (như đối tượng)" cho hiệp phương sai và: "API được mong đợi một cái gì đó chung chung (như đối tượng), bạn có thể cung cấp cho nó một cái gì đó cụ thể hơn (như string)" cho contravariance . Đối với tôi những loại âm thanh giống nhau!
XMight

@AlexanderDerck: Không chắc tại sao tôi không trả lời bạn trước đó; Tôi đồng ý nó không rõ ràng, và sẽ cố gắng làm rõ nó.
Jon Skeet

16

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.


Bạn nói ghi đè của một kiểu con có thể trả về một loại chuyên dụng hơn . Nhưng điều đó là hoàn toàn sai sự thật. Nếu 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.
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() 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!
Nico

1
Tương tự như nhận xét của Matt-Klein về câu trả lời của @ Jon-Skeet, "với bản thân tương lai của tôi": Điều tuyệt vời nhất đối với tôi ở đây là "Họ cung cấp nhiều hơn" (cụ thể) và "Họ yêu cầu ít hơn" (cụ thể). "Yêu cầu ít hơn và cung cấp nhiều hơn" là một ghi nhớ tuyệt vời! Nó tương tự như một công việc mà tôi hy vọng sẽ yêu cầu các hướng dẫn ít cụ thể hơn (yêu cầu chung) và vẫn cung cấp một cái gì đó cụ thể hơn (một sản phẩm công việc thực tế). Dù bằng cách nào, thứ tự của các kiểu con (LSP) không bị phá vỡ.
karfus

@karfus: Cảm ơn bạn, nhưng như tôi nhớ, tôi đã diễn giải ý tưởng "Yêu cầu ít hơn và cung cấp nhiều hơn" từ một nguồn khác. Có thể đó là cuốn sách của Liu mà tôi đề cập ở trên ... hoặc thậm chí là một bài nói chuyện .NET Rock. Btw. trong Java, mọi người đã giảm tính ghi nhớ thành "PECS", liên quan trực tiếp đến cách cú pháp để khai báo phương sai, PECS dành cho "Nhà sản xuất extends, Người tiêu dùng super".
Nico

5

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();

0

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ụ.

Hiệp phương sai

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 đó Cmột lớp con của B, nếu Atạ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 outcho 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 chỉ định

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 Clà lớp con của B, nếu Atiê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());

Liên kết

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.