Covariance, Invariance และ Contravariance อธิบายเป็นภาษาอังกฤษล้วน?


113

วันนี้ผมอ่านบทความเกี่ยวกับ Covariance, Contravariance (และ Invariance) ใน Java ฉันอ่านบทความ Wikipedia ภาษาอังกฤษและภาษาเยอรมันรวมถึงบล็อกโพสต์และบทความอื่น ๆ จาก IBM

แต่ฉันยังสับสนเล็กน้อยว่ามันเกี่ยวกับอะไรกันแน่? บางคนบอกว่ามันเกี่ยวกับความสัมพันธ์ระหว่างประเภทและประเภทย่อยบางคนบอกว่ามันเกี่ยวกับการแปลงประเภทและบางคนบอกว่ามันใช้เพื่อตัดสินใจว่าเมธอดจะถูกลบล้างหรือโอเวอร์โหลด

ดังนั้นฉันกำลังมองหาคำอธิบายง่ายๆเป็นภาษาอังกฤษธรรมดาที่แสดงให้ผู้เริ่มต้นเห็นว่า Covariance and Contravariance (และ Invariance) คืออะไร จุดบวกสำหรับตัวอย่างง่ายๆ


โปรดดูโพสต์นี้ซึ่งอาจเป็นประโยชน์สำหรับคุณ: stackoverflow.com/q/2501023/218717
Francisco Alvarado

3
บางทีคำถามประเภทการแลกเปลี่ยนสแต็กของโปรแกรมเมอร์จะดีกว่า หากคุณโพสต์ที่นั่นให้พิจารณาระบุสิ่งที่คุณเข้าใจและสิ่งที่ทำให้คุณสับสนโดยเฉพาะเพราะตอนนี้คุณกำลังขอให้ใครสักคนเขียนบทแนะนำทั้งหมดให้คุณอีกครั้ง
Hovercraft Full Of Eels

คำตอบ:


289

บางคนบอกว่ามันเกี่ยวกับความสัมพันธ์ระหว่างประเภทและประเภทย่อยบางคนบอกว่ามันเกี่ยวกับการแปลงประเภทและคนอื่น ๆ บอกว่ามันถูกใช้เพื่อตัดสินใจว่าเมธอดถูกเขียนทับหรือโอเวอร์โหลด

ทั้งหมดที่กล่าวมา

หัวใจสำคัญคำเหล่านี้อธิบายว่าความสัมพันธ์ประเภทย่อยได้รับผลกระทบจากการแปลงประเภทอย่างไร นั่นคือถ้าAและBเป็นประเภทfคือการแปลงประเภทและ≤ความสัมพันธ์ประเภทย่อย (เช่นA ≤ Bหมายความว่าAเป็นประเภทย่อยของB) เรามี

  • fเป็นโรคโควาเรียหากA ≤ Bมีนัยอย่างนั้นf(A) ≤ f(B)
  • fเป็นสิ่งที่ตรงกันข้ามหากA ≤ Bกล่าวเป็นนัยว่าf(B) ≤ f(A)
  • f จะไม่เปลี่ยนแปลงหากไม่มีการระงับทั้งสองข้อข้างต้น

ลองพิจารณาตัวอย่าง Let f(A) = List<A>ที่Listถูกประกาศโดย

class List<T> { ... } 

คือfcovariant, contravariant หรือคงที่? Covariant จะหมายความว่า a List<String>เป็นประเภทย่อยของList<Object>contravariant ที่ a List<Object>เป็นประเภทย่อยของList<String>และไม่แปรผันที่ทั้งสองไม่ได้เป็นประเภทย่อยของอีกประเภทหนึ่งกล่าวคือList<String>และList<Object>เป็นประเภทที่ไม่สามารถแปลงกลับได้ ใน Java คำหลังเป็นจริงเราพูด (ค่อนข้างไม่เป็นทางการ) ว่ายาสามัญไม่แปรผัน

ตัวอย่างอื่น. ให้f(A) = A[]. คือfcovariant, contravariant หรือคงที่? นั่นคือ String [] เป็นประเภทย่อยของ Object [], Object [] ประเภทย่อยของ String [] หรือไม่ใช่ประเภทย่อยของอีกประเภทหนึ่ง? (คำตอบ: ใน Java อาร์เรย์เป็นโควาเรียร์)

สิ่งนี้ยังค่อนข้างเป็นนามธรรม เพื่อให้เป็นรูปธรรมมากขึ้นมาดูกันว่าการดำเนินการใดใน Java ถูกกำหนดในแง่ของความสัมพันธ์ประเภทย่อย ตัวอย่างที่ง่ายที่สุดคือการมอบหมายงาน คำสั่ง

x = y;

typeof(y) ≤ typeof(x)จะรวบรวมเฉพาะในกรณีที่ นั่นคือเราเพิ่งได้เรียนรู้ว่างบ

ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();

จะไม่คอมไพล์ใน Java แต่

Object[] objects = new String[1];

จะ.

อีกตัวอย่างหนึ่งที่ความสัมพันธ์ประเภทย่อยมีความสำคัญคือนิพจน์การเรียกใช้เมธอด:

result = method(a);

ทางการพูดคำสั่งนี้จะถูกประเมินโดยการกำหนดค่าของพารามิเตอร์แรกของวิธีการดำเนินการแล้วร่างกายของวิธีการและจากนั้นการกำหนดวิธีการที่จะส่งกลับค่าa resultเช่นเดียวกับที่ได้รับมอบหมายธรรมดาในตัวอย่างที่ผ่านมา "ด้านขวามือ" จะต้องเป็นประเภทย่อยของ "ด้านซ้ายมือ" คือคำสั่งนี้จะไม่สามารถใช้งานได้ถ้าและtypeof(a) ≤ typeof(parameter(method)) returntype(method) ≤ typeof(result)นั่นคือถ้าวิธีการถูกประกาศโดย:

Number[] method(ArrayList<Number> list) { ... }

ไม่มีนิพจน์ใดต่อไปนี้ที่จะรวบรวม:

Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());

แต่

Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());

จะ.

อีกตัวอย่างหนึ่งที่การพิมพ์ย่อยมีผลเหนือกว่า พิจารณา:

Super sup = new Sub();
Number n = sup.method(1);

ที่ไหน

class Super {
    Number method(Number n) { ... }
}

class Sub extends Super {
    @Override 
    Number method(Number n);
}

ไม่เป็นทางการรันไทม์จะเขียนสิ่งนี้ใหม่เป็น:

class Super {
    Number method(Number n) {
        if (this instanceof Sub) {
            return ((Sub) this).method(n);  // *
        } else {
            ... 
        }
    }
}

สำหรับบรรทัดที่ทำเครื่องหมายเพื่อคอมไพล์พารามิเตอร์วิธีการของเมธอดการลบล้างจะต้องเป็นซูเปอร์ไทป์ของพารามิเตอร์วิธีการของเมธอดที่ถูกแทนที่และประเภทส่งคืนเป็นประเภทย่อยของเมธอดที่ถูกแทนที่ การพูดอย่างเป็นทางการf(A) = parametertype(method asdeclaredin(A))อย่างน้อยต้องมีความแตกต่างและf(A) = returntype(method asdeclaredin(A))อย่างน้อยก็ต้องเป็นโรคโควาเรีย

สังเกต "อย่างน้อย" ด้านบน สิ่งเหล่านี้เป็นข้อกำหนดขั้นต่ำที่เหมาะสมสำหรับภาษาการเขียนโปรแกรมเชิงวัตถุประเภทคงที่ที่เหมาะสมจะบังคับใช้ แต่ภาษาโปรแกรมอาจเลือกที่จะเข้มงวดมากขึ้น ในกรณีของ Java 1.4 ประเภทพารามิเตอร์และประเภทการส่งคืนเมธอดจะต้องเหมือนกัน (ยกเว้นการลบประเภท) เมื่อแทนที่เมธอดกล่าวคือparametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))เมื่อแทนที่ เนื่องจาก Java 1.5 จึงอนุญาตให้ใช้ชนิดการส่งคืนโควาเรียนได้เมื่อทำการลบล้างกล่าวคือต่อไปนี้จะคอมไพล์ใน Java 1.5 แต่ไม่ใช่ใน Java 1.4:

class Collection {
    Iterator iterator() { ... }
}

class List extends Collection {
    @Override 
    ListIterator iterator() { ... }
}

ฉันหวังว่าฉันจะครอบคลุมทุกอย่าง - หรือเป็นรอยขีดข่วนบนพื้นผิว ฉันยังหวังว่ามันจะช่วยให้เข้าใจนามธรรม แต่แนวคิดที่สำคัญของความแปรปรวนประเภท


1
นอกจากนี้เนื่องจากประเภทอาร์กิวเมนต์ที่ไม่ตรงกันของ Java 1.5 ได้รับอนุญาตเมื่อทำการลบล้าง ฉันคิดว่าคุณพลาดสิ่งนั้น
Brian Gordon

13
ที่พวกเขา? ฉันเพิ่งลองใช้ใน eclipse และคอมไพเลอร์คิดว่าฉันตั้งใจจะโอเวอร์โหลดมากกว่าที่จะลบล้างและปฏิเสธโค้ดเมื่อฉันวางคำอธิบายประกอบ @Override บนเมธอดคลาสย่อย คุณมีหลักฐานใด ๆ ที่อ้างว่า Java สนับสนุนประเภทอาร์กิวเมนต์ที่ไม่ตรงกันหรือไม่?
ทำบุญ

1
คุณพูดถูก ฉันเชื่อใครบางคนโดยไม่ได้ตรวจสอบตัวเอง
ไบรอันกอร์ดอน

1
ฉันอ่านเอกสารมากมายและดูการพูดคุยเกี่ยวกับหัวข้อนี้เล็กน้อย แต่นี่เป็นคำอธิบายที่ดีที่สุด ขอบคุณมาก
minzchickenflavour

1
+1 สำหรับการเป็นอย่าง Leman A ≤ Bและเรียบง่ายด้วย สัญกรณ์ดังกล่าวทำให้สิ่งต่างๆเรียบง่ายและมีความหมายมากขึ้น อ่านดี ...
Romeo Sierra

12

การใช้ระบบประเภท java จากนั้นเรียน:

อ็อบเจ็กต์บางประเภท T สามารถแทนที่ด้วยอ็อบเจ็กต์ประเภทย่อยของ T.

ความแตกต่างของประเภท - วิธีการเรียนมีเงื่อนไขดังต่อไปนี้

class A {
    public S f(U u) { ... }
}

class B extends A {
    @Override
    public T f(V v) { ... }
}

B b = new B();
t = b.f(v);
A a = ...; // Might have type B
s = a.f(u); // and then do V v = u;

จะเห็นได้ว่า:

  • T ต้องเป็นประเภทย่อย S ( covariant เนื่องจาก B เป็นประเภทย่อยของ A )
  • วีจะต้อง supertype ของ U ( contravariantเป็นทิศทางตรงกันข้ามมรดก)

ตอนนี้ร่วมและตรงกันข้ามกับ B เป็นประเภทย่อยของ A. การพิมพ์ที่แข็งแกร่งขึ้นต่อไปนี้อาจได้รับการแนะนำด้วยความรู้ที่เฉพาะเจาะจงมากขึ้น ในประเภทย่อย

ความแปรปรวนร่วม (พร้อมใช้งานใน Java) มีประโยชน์ในการบอกว่าผลลัพธ์ที่เจาะจงมากขึ้นในประเภทย่อยนั้น โดยเฉพาะเมื่อ A = T และ B = S ความแตกต่างบอกว่าคุณพร้อมที่จะจัดการกับข้อโต้แย้งที่กว้างขึ้น


9

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

ความแปรปรวนร่วมและความแตกต่างเป็นสิ่งที่สมเหตุสมผล ระบบประเภทภาษาบังคับให้เราสนับสนุนตรรกะในชีวิตจริง เป็นตัวอย่างที่เข้าใจง่าย

แปรปรวน

ตัวอย่างเช่นคุณต้องการซื้อดอกไม้และคุณมีร้านดอกไม้สองแห่งในเมืองของคุณ: ร้านกุหลาบและร้านเดซี่

หากคุณถามใครบางคนว่า "ร้านดอกไม้อยู่ที่ไหน" และมีคนบอกคุณว่าร้านกุหลาบอยู่ที่ไหนจะเป็นไรไหม? ใช่เพราะกุหลาบเป็นดอกไม้ถ้าคุณต้องการซื้อดอกไม้คุณสามารถซื้อดอกกุหลาบได้ เช่นเดียวกันหากมีคนตอบกลับคุณพร้อมแจ้งที่อยู่ของร้านเดซี่ นี่คือตัวอย่างของความแปรปรวนร่วม : คุณได้รับอนุญาตให้ส่งA<C>ไปยังคลาสย่อยของถ้าA<B>อยู่ที่ไหนCBAผลิตค่าทั่วไป (ผลตอบแทนที่เป็นผลจากการทำงานที่) ความแปรปรวนร่วมเป็นเรื่องของผู้ผลิต

ประเภท:

class Flower {  }
class Rose extends Flower { }
class Daisy extends Flower { }

interface FlowerShop<T extends Flower> {
    T getFlower();
}

class RoseShop implements FlowerShop<Rose> {
    @Override
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop implements FlowerShop<Daisy> {
    @Override
    public Daisy getFlower() {
        return new Daisy();
    }
}

คำถามคือ "ร้านดอกไม้อยู่ที่ไหน" คำตอบคือ "ร้านกุหลาบที่นั่น":

static FlowerShop<? extends Flower> tellMeShopAddress() {
    return new RoseShop();
}

contravariance

เช่นคุณต้องการมอบดอกไม้ให้แฟนของคุณ หากแฟนของคุณชอบดอกไม้ชนิดใดคุณสามารถมองว่าเธอเป็นคนที่รักดอกกุหลาบหรือเป็นคนที่ชอบดอกเดซี่? ใช่เพราะถ้าเธอรักดอกไม้เธอก็จะรักทั้งกุหลาบและเดซี่ นี่คือตัวอย่างของที่contravariance : คุณได้รับอนุญาตให้โยนA<B>ไปA<C>ที่Cเป็น subclass ของBถ้าAกินค่าทั่วไป ความแตกต่างเป็นเรื่องของผู้บริโภค

ประเภท:

interface PrettyGirl<TFavouriteFlower extends Flower> {
    void takeGift(TFavouriteFlower flower);
}

class AnyFlowerLover implements PrettyGirl<Flower> {
    @Override
    public void takeGift(Flower flower) {
        System.out.println("I like all flowers!");
    }

}

คุณกำลังพิจารณาแฟนของคุณที่ชอบดอกไม้ใด ๆ เหมือนคนที่รักดอกกุหลาบและมอบดอกกุหลาบให้เธอ:

PrettyGirl<? super Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

คุณสามารถค้นหาข้อมูลเพิ่มเติมได้ที่แหล่งที่มา


@ ปีเตอร์ขอบคุณมันเป็นจุดที่ยุติธรรม ความคงตัวคือเมื่อไม่มีความสัมพันธ์ระหว่างคลาสที่มีพารามิเตอร์ทั่วไปที่แตกต่างกันกล่าวคือคุณไม่สามารถส่ง A <B> เป็น A <C> ว่าความสัมพันธ์ระหว่าง B และ C คืออะไร
VadzimV
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.