การทำความเข้าใจอินเทอร์เฟซ Covariant และ Contravariant ใน C #


87

ฉันเจอสิ่งเหล่านี้ในหนังสือเรียนที่ฉันอ่านเรื่อง C # แต่ฉันมีปัญหาในการทำความเข้าใจอาจเนื่องมาจากไม่มีบริบท

มีคำอธิบายที่สั้นกระชับเกี่ยวกับสิ่งที่พวกเขาคืออะไรและสิ่งที่มีประโยชน์สำหรับที่นั่นหรือไม่?

แก้ไขเพื่อความกระจ่าง:

อินเตอร์เฟซ Covariant:

interface IBibble<out T>
.
.

อินเทอร์เฟซ Contravariant:

interface IBibble<in T>
.
.

3
นี่คือคำอธิบายสั้น ๆ และดี IMHO: blogs.msdn.com/csharpfaq/archive/2010/02/16/…
digEmAll

อาจเป็นประโยชน์: Blog Post
Krunal

อืมเป็นเรื่องดี แต่ไม่ได้อธิบายว่าทำไมสิ่งที่ทำให้ฉันงงงวยจริงๆ
NibblyPig

คำตอบ:


144

ด้วย<out T>คุณสามารถถือว่าการอ้างอิงอินเทอร์เฟซเป็นหนึ่งในลำดับชั้น

ด้วย<in T>คุณสามารถใช้การอ้างอิงอินเทอร์เฟซเป็นแบบหนึ่งใน hiearchy

ให้ฉันพยายามอธิบายเป็นภาษาอังกฤษเพิ่มเติม

สมมติว่าคุณกำลังเรียกดูรายชื่อสัตว์จากสวนสัตว์ของคุณและคุณตั้งใจจะดำเนินการให้ สัตว์ทุกตัว (ในสวนสัตว์ของคุณ) มีชื่อและรหัสเฉพาะ สัตว์บางชนิดเป็นสัตว์เลี้ยงลูกด้วยนมบางชนิดเป็นสัตว์เลื้อยคลานบางชนิดเป็นสัตว์ครึ่งบกครึ่งน้ำบางชนิดเป็นปลา ฯลฯ แต่เป็นสัตว์ทั้งหมด

ดังนั้นด้วยรายชื่อสัตว์ของคุณ (ซึ่งมีสัตว์ประเภทต่างๆ) คุณสามารถพูดได้ว่าสัตว์ทุกตัวมีชื่อดังนั้นจึงปลอดภัยที่จะได้รับชื่อสัตว์ทั้งหมด

อย่างไรก็ตามถ้าคุณมีรายชื่อปลาเท่านั้น แต่ต้องปฏิบัติต่อพวกมันเหมือนสัตว์ล่ะจะได้ผลหรือไม่? โดยสัญชาตญาณควรใช้งานได้ แต่ใน C # 3.0 และก่อนหน้านี้ส่วนของโค้ดจะไม่รวบรวม:

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

เหตุผลก็คือคอมไพเลอร์ไม่ "รู้" ว่าคุณตั้งใจหรือสามารถทำอะไรกับคอลเลกชันสัตว์หลังจากที่คุณได้รับมันแล้ว สำหรับทุกสิ่งที่ทราบอาจมีวิธีIEnumerable<T>การนำวัตถุกลับเข้าไปในรายการและอาจทำให้คุณสามารถใส่สัตว์ที่ไม่ใช่ปลาลงในคอลเลกชันที่ควรจะมีปลาเท่านั้น

กล่าวอีกนัยหนึ่งคอมไพเลอร์ไม่สามารถรับประกันได้ว่าสิ่งนี้ไม่ได้รับอนุญาต:

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

ดังนั้นคอมไพเลอร์จึงปฏิเสธที่จะรวบรวมโค้ดของคุณทันที นี่คือความแปรปรวนร่วม

มาดูความแตกต่างกัน

เนื่องจากสวนสัตว์ของเราสามารถรองรับสัตว์ได้ทุกชนิดจึงสามารถจับปลาได้อย่างแน่นอนดังนั้นเรามาลองเพิ่มปลาในสวนสัตว์ของเรากันดีกว่า

ใน C # 3.0 และก่อนหน้านี้จะไม่รวบรวม:

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

ที่นี่คอมไพเลอร์สามารถอนุญาตโค้ดชิ้นนี้ได้แม้ว่าเมธอดจะคืนค่าList<Animal>เพียงเพราะปลาทั้งหมดเป็นสัตว์ดังนั้นหากเราเปลี่ยนประเภทเป็นสิ่งนี้:

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

จากนั้นจะใช้งานได้ แต่คอมไพเลอร์ไม่สามารถระบุได้ว่าคุณไม่ได้พยายามทำสิ่งนี้:

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

เนื่องจากรายการนี้เป็นรายชื่อสัตว์จึงไม่อนุญาต

ดังนั้นความแตกต่างและความแปรปรวนร่วมคือวิธีที่คุณปฏิบัติต่อการอ้างอิงอ็อบเจ็กต์และสิ่งที่คุณได้รับอนุญาตให้ทำกับสิ่งเหล่านี้

inและoutคำหลักใน C # 4.0 เครื่องหมายโดยเฉพาะอินเตอร์เฟซเป็นหนึ่งหรืออื่น ๆ ด้วยinคุณได้รับอนุญาตให้วางประเภททั่วไป (โดยปกติคือ T) ในตำแหน่งอินพุตซึ่งหมายถึงอาร์กิวเมนต์ของวิธีการและคุณสมบัติเขียนอย่างเดียว

ด้วยoutคุณได้รับอนุญาตให้วางประเภททั่วไปในเอาต์พุต -ตำแหน่งซึ่งเป็นวิธีการคืนค่าคุณสมบัติอ่านอย่างเดียวและพารามิเตอร์วิธีการออก

สิ่งนี้จะช่วยให้คุณทำสิ่งที่ตั้งใจจะทำกับโค้ด:

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> มีทั้งในและนอกทิศทางบน T ดังนั้นจึงไม่ใช่ตัวแปรร่วมหรือตัวแปรตรงกันข้าม แต่เป็นอินเทอร์เฟซที่อนุญาตให้คุณเพิ่มวัตถุเช่นนี้:

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

จะอนุญาตให้คุณทำสิ่งนี้:

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

นี่คือวิดีโอบางส่วนที่แสดงแนวคิด:

นี่คือตัวอย่าง:

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;
        }
    }
}

หากไม่มีเครื่องหมายเหล่านี้สิ่งต่อไปนี้สามารถรวบรวม:

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

หรือสิ่งนี้:

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

อืมคุณสามารถอธิบายเป้าหมายของความแปรปรวนร่วมและความแปรปรวนได้หรือไม่? มันอาจช่วยให้ฉันเข้าใจมันมากขึ้น
NibblyPig

1
ดูบิตสุดท้ายซึ่งเป็นสิ่งที่คอมไพเลอร์ป้องกันไว้ก่อนหน้านี้จุดประสงค์ของการเข้าและออกคือการบอกว่าคุณสามารถทำอะไรกับอินเทอร์เฟซ (หรือประเภท) ที่ปลอดภัยเพื่อที่คอมไพลเลอร์จะไม่ป้องกันไม่ให้คุณทำสิ่งที่ปลอดภัย .
Lasse V.Karlsen

คำตอบที่ยอดเยี่ยมฉันดูวิดีโอที่มีประโยชน์มากและรวมกับตัวอย่างของคุณที่ฉันจัดเรียงไว้แล้ว มีเพียงคำถามเดียวที่ยังคงอยู่และนั่นคือสาเหตุที่ต้องใช้คำว่า "out" และ "in" ทำไมสตูดิโอภาพไม่ทราบโดยอัตโนมัติว่าคุณกำลังพยายามทำอะไรอยู่ (หรือเหตุผลเบื้องหลัง) คืออะไร?
NibblyPig

Automagic "ฉันเห็นสิ่งที่คุณกำลังพยายามทำอยู่" โดยปกติจะมีการขมวดคิ้วเมื่อต้องประกาศเรื่องต่างๆเช่นคลาสจะดีกว่าถ้าให้โปรแกรมเมอร์ทำเครื่องหมายประเภทอย่างชัดเจน คุณสามารถลองเพิ่ม "in" ลงในคลาสที่มีเมธอดที่ส่งคืน T และคอมไพเลอร์จะบ่น ลองนึกภาพว่าจะเกิดอะไรขึ้นถ้าเพียงแค่ลบ "in" ที่เพิ่มไว้ให้คุณโดยอัตโนมัติ
Lasse V.Karlsen

1
หากคำหลักหนึ่งคำต้องการคำอธิบายที่ยาวแสดงว่ามีบางอย่างผิดปกติ ในความคิดของฉัน C # พยายามที่จะฉลาดเกินไปในกรณีนี้ อย่างไรก็ตามขอขอบคุณสำหรับคำอธิบายที่น่าสนใจ
rr-

8

โพสต์นี้ดีที่สุดที่ฉันเคยอ่านในหัวข้อนี้

ในระยะสั้นความแปรปรวนร่วม / ความแตกต่าง / ความไม่แปรเปลี่ยนจะเกี่ยวข้องกับการหล่อแบบอัตโนมัติ (จากฐานไปยังฐานที่ได้รับและในทางกลับกัน) การแคสประเภทเหล่านี้จะเกิดขึ้นได้ก็ต่อเมื่อการค้ำประกันบางอย่างได้รับการเคารพในแง่ของการอ่าน / เขียนที่ดำเนินการกับวัตถุที่ถูกร่าย อ่านโพสต์เพื่อดูรายละเอียดเพิ่มเติม


5
ลิงค์ดูเหมือนจะตาย นี่คือเวอร์ชันที่เก็บถาวร: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618

1
ฉันชอบคำอธิบายนี้มากขึ้น: codepureandsimple.com/…
volkit
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.