Hiểu giao diện Covariant và Contravariant trong C #


86

Tôi đã bắt gặp những điều này trong một cuốn sách giáo khoa tôi đang đọc về C #, nhưng tôi gặp khó khăn trong việc hiểu chúng, có thể là do thiếu ngữ cảnh.

Có lời giải thích ngắn gọn tốt về chúng là gì và chúng hữu ích cho việc gì không?

Chỉnh sửa để làm rõ:

Giao diện covariant:

interface IBibble<out T>
.
.

Giao diện tương phản:

interface IBibble<in T>
.
.

3
Đây là một IMHO expalnation ngắn và tốt: blogs.msdn.com/csharpfaq/archive/2010/02/16/...
digEmAll

Có thể hữu ích: Bài đăng trên blog
Krunal

Hmm, nó là tốt nhưng nó không giải thích tại sao đó là những gì thực sự làm tôi bối rối.
NibblyPig

Câu trả lời:


144

Với <out T>, bạn có thể coi tham chiếu giao diện như một tham chiếu trở lên trong hệ thống phân cấp.

Với <in T>, bạn có thể coi tham chiếu giao diện như một tham chiếu hướng xuống trong quá trình tìm kiếm.

Hãy để tôi cố gắng giải thích nó bằng nhiều thuật ngữ tiếng Anh hơn.

Giả sử bạn đang truy xuất danh sách các loài động vật từ vườn thú của mình và bạn định xử lý chúng. Tất cả động vật (trong vườn thú của bạn) đều có tên và ID duy nhất. Một số động vật là động vật có vú, một số là bò sát, một số là lưỡng cư, một số là cá, v.v. nhưng chúng đều là động vật.

Vì vậy, với danh sách động vật của bạn (bao gồm các loài động vật khác nhau), bạn có thể nói rằng tất cả các loài động vật đều có tên, vì vậy rõ ràng là sẽ an toàn nếu lấy tên của tất cả các loài động vật.

Tuy nhiên, nếu bạn chỉ có một danh sách các loài cá, nhưng cần phải đối xử với chúng như động vật, thì điều đó có hiệu quả không? Theo trực giác, nó sẽ hoạt động, nhưng trong C # 3.0 trở về trước, đoạn mã này sẽ không biên dịch:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

Lý do cho điều này là trình biên dịch không "biết" bạn dự định hoặc có thể làm gì với bộ sưu tập động vật sau khi bạn đã truy xuất nó. Đối với tất cả những gì nó biết, có thể có một cách IEnumerable<T>để đưa một đối tượng trở lại danh sách và điều đó có khả năng cho phép bạn đưa một con vật không phải là cá vào một bộ sưu tập được cho là chỉ chứa cá.

Nói cách khác, trình biên dịch không thể đảm bảo rằng điều này không được phép:

animals.Add(new Mammal("Zebra"));

Vì vậy, trình biên dịch hoàn toàn từ chối biên dịch mã của bạn. Đây là hiệp phương sai.

Hãy nhìn vào sự tương phản.

Vì vườn thú của chúng ta có thể xử lý tất cả các loài động vật, nó chắc chắn có thể xử lý cá, vì vậy chúng ta hãy thử thêm một số loài cá vào vườn thú của chúng ta.

Trong C # 3.0 trở về trước, điều này không biên dịch:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

Ở đây, trình biên dịch có thể cho phép đoạn mã này, mặc dù phương thức trả về List<Animal>đơn giản vì tất cả các loài cá đều là động vật, vì vậy nếu chúng ta chỉ thay đổi các loại thành này:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

Sau đó, nó sẽ hoạt động, nhưng trình biên dịch không thể xác định rằng bạn không cố gắng làm điều này:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

Vì danh sách thực sự là danh sách động vật, điều này không được phép.

Vì vậy, tương phản và đồng phương sai là cách bạn xử lý các tham chiếu đối tượng và những gì bạn được phép làm với chúng.

Các từ khóa inouttrong C # 4.0 đặc biệt đánh dấu giao diện là giao diện này hoặc giao diện khác. Với in, bạn được phép đặt kiểu chung (thường là T) trong các vị trí đầu vào , có nghĩa là đối số phương thức và thuộc tính chỉ ghi.

Với out, bạn được phép đặt kiểu chung trong các vị trí đầu ra , là giá trị trả về của phương thức, thuộc tính chỉ đọc và tham số phương thức ra.

Điều này sẽ cho phép bạn thực hiện những gì dự định làm với mã:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T> có cả hướng vào và hướng ra trên T, vì vậy nó không phải là đồng biến thể cũng không phải là biến thể đối lập, mà là một giao diện cho phép bạn thêm các đối tượng, như sau:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

sẽ cho phép bạn làm điều này:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

Dưới đây là một số video thể hiện các khái niệm:

Đây là một ví dụ:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

Nếu không có những dấu này, phần sau có thể biên dịch:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

hoặc cái này:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants

Hmm, bạn có thể giải thích mục tiêu của hiệp phương sai và phương sai không? Nó có thể giúp tôi hiểu nó nhiều hơn.
NibblyPig

1
Xem phần cuối cùng, đó là điều mà trình biên dịch đã ngăn chặn trước đây, mục đích của việc vào và ra là để nói những gì bạn có thể làm với các giao diện (hoặc kiểu) an toàn, để trình biên dịch không ngăn cản bạn thực hiện những điều an toàn .
Lasse V. Karlsen

Câu trả lời tuyệt vời, tôi đã xem những video chúng rất hữu ích và kết hợp với ví dụ của bạn, tôi đã sắp xếp nó ngay bây giờ. Chỉ còn lại một câu hỏi, đó là lý do tại sao bắt buộc phải có 'out' và 'in', tại sao visual studio không tự động biết bạn đang cố gắng làm gì (hoặc lý do đằng sau nó là gì)?
NibblyPig

Tự động hóa "Tôi thấy những gì bạn đang cố gắng làm ở đó" thường bị khó chịu khi nói đến việc khai báo những thứ như lớp, tốt hơn là để lập trình viên đánh dấu các loại một cách rõ ràng. Bạn có thể thử thêm "in" vào một lớp có các phương thức trả về T và trình biên dịch sẽ phàn nàn. Hãy tưởng tượng điều gì sẽ xảy ra nếu nó âm thầm loại bỏ "in" mà trước đó nó đã tự động thêm cho bạn.
Lasse V. Karlsen

1
Nếu một từ khóa cần giải thích dài dòng này, thì rõ ràng có điều gì đó không ổn. Theo tôi, C # đang cố tỏ ra quá thông minh trong trường hợp cụ thể này. Tuy nhiên, cảm ơn bạn đã giải thích thú vị.
rr- Ngày

7

Bài đăng này là hay nhất mà tôi đã đọc về chủ đề này

Nói tóm lại, hiệp phương sai / tương phản / bất biến giải quyết việc ép kiểu tự động (từ cơ sở đến dẫn xuất và ngược lại). Những kiểu đúc đó chỉ có thể thực hiện được nếu một số đảm bảo được tôn trọng về các hành động đọc / ghi được thực hiện trên các đối tượng được ép kiểu. Đọc bài đăng để biết thêm chi tiết.


5
Liên kết dường như đã chết. Dưới đây là một phiên bản lưu trữ: web.archive.org/web/20140626123445/http://adamnathan.co.uk/...
si618

1
tôi thích lời giải thích này hơn nữa: codepureandsimple.com/…
volkit
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.