เพราะเหตุใดตัวอย่างจึงไม่ได้รับการรวบรวมความแปรปรวน (ร่วมต้านและและ) จึงทำงานได้อย่างไร


147

การติดตามจากคำถามนี้มีใครช่วยอธิบายสิ่งต่อไปนี้ในสกาลา:

class Slot[+T] (var some: T) { 
   //  DOES NOT COMPILE 
   //  "COVARIANT parameter in CONTRAVARIANT position"

}

ฉันเข้าใจความแตกต่างระหว่าง+TและTในการประกาศประเภท (มันรวบรวมถ้าฉันใช้T) แต่แล้วเราจะเขียนคลาสที่ covariant ในพารามิเตอร์ประเภทของมันโดยไม่ต้องใช้วิธีการสร้างสิ่งที่ไม่มีใครเทียบได้อย่างไร ฉันสามารถมั่นใจได้ว่าต่อไปนี้เท่านั้นที่สามารถสร้างขึ้นด้วยตัวอย่างของT?

class Slot[+T] (var some: Object){    
  def get() = { some.asInstanceOf[T] }
}

แก้ไข - ตอนนี้ลงไปที่ต่อไปนี้:

abstract class _Slot[+T, V <: T] (var some: V) {
    def getT() = { some }
}

ทั้งหมดนี้เป็นสิ่งที่ดี แต่ตอนนี้ฉันมีพารามิเตอร์ประเภทที่สองที่ฉันต้องการเพียงหนึ่ง ฉันจะถามคำถามอีกครั้งดังนี้:

ฉันจะเขียนคลาสที่ไม่เปลี่ยนรูป Slotซึ่งเป็นcovariantในประเภทของมันได้อย่างไร?

แก้ไข 2 : Duh! ผมใช้และไม่ได้var valต่อไปนี้เป็นสิ่งที่ฉันต้องการ:

class Slot[+T] (val some: T) { 
}

6
เพราะvarเป็นตัวอย่างในขณะที่valไม่ได้ มันเป็นเหตุผลเดียวกันกับที่คอลเลคชั่นที่ไม่เปลี่ยนรูปแบบของสกาล่านั้นแปรเปลี่ยนไป แต่สิ่งที่เปลี่ยนแปลงไม่ได้
oxbow_lakes

สิ่งนี้อาจน่าสนใจในบริบทนี้: scala-lang.org/old/node/129
user573215

คำตอบ:


302

โดยทั่วไปพารามิเตอร์ประเภทcovariantเป็นสิ่งที่ได้รับอนุญาตให้แตกต่างกันไปตามคลาสที่ถูก subtyped (หรือเปลี่ยนแปลงด้วย subtyping ด้วยเหตุนี้คำนำหน้า "co-") เป็นรูปธรรมมากขึ้น:

trait List[+A]

List[Int]เป็นชนิดย่อยของList[AnyVal]เพราะเป็นชนิดย่อยของInt AnyValซึ่งหมายความว่าคุณอาจระบุอินสแตนซ์ที่คาดว่าจะList[Int]มีค่าประเภทเมื่อList[AnyVal]ใด นี่เป็นวิธีที่ใช้งานง่ายมากสำหรับ generics ในการทำงาน แต่ปรากฎว่ามันไม่ปลอดภัย (แบ่งระบบประเภท) เมื่อใช้ต่อหน้าข้อมูลที่ไม่แน่นอน นี่คือเหตุผลที่ generics ไม่แปรเปลี่ยนใน Java ตัวอย่างสั้น ๆ ของความไม่มั่นคงโดยใช้อาร์เรย์ Java (ซึ่งแปรปรวนร่วมอย่างไม่ถูกต้อง):

Object[] arr = new Integer[1];
arr[0] = "Hello, there!";

เราเพียงแค่กำหนดค่าของชนิดไปยังอาร์เรย์ของพิมพ์String Integer[]ด้วยเหตุผลที่ควรชัดเจนนี่เป็นข่าวร้าย ระบบชนิดของ Java อนุญาตให้ทำได้ในเวลารวบรวม JVM จะ "ช่วย" โยนArrayStoreExceptionat runtime ระบบชนิดของ Scala ป้องกันปัญหานี้ได้เนื่องจากพารามิเตอร์ type บนArrayคลาสไม่แปรเปลี่ยน (การประกาศ[A]มากกว่า[+A])

ทราบว่ามีประเภทของความแปรปรวนอื่นที่รู้จักกันเป็นcontravariance สิ่งนี้สำคัญมากเพราะอธิบายได้ว่าทำไมความแปรปรวนร่วมอาจทำให้เกิดปัญหาบางอย่าง ความแปรปรวนเป็นอักษรตรงข้ามของความแปรปรวนร่วม: พารามิเตอร์แปรผันขึ้นกับการพิมพ์ย่อย มันเป็นเรื่องธรรมดาน้อยกว่าบางส่วนเพราะมันตอบโต้ได้ง่ายแม้ว่ามันจะมีแอพพลิเคชั่นที่สำคัญมากอย่างหนึ่ง: ฟังก์ชั่น

trait Function1[-P, +R] {
  def apply(p: P): R
}

สังเกตเห็นคำอธิบายประกอบ" - " ความแปรปรวนในPพารามิเตอร์ชนิด ประกาศนี้เป็นวิธีการทั้งที่Function1เป็น contravariant ในPและ covariant Rใน ดังนั้นเราสามารถได้รับสัจพจน์ต่อไปนี้:

T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']

ขอให้สังเกตว่าT1'จะต้องเป็นชนิดย่อย (หรือพิมพ์เดียวกัน) ของT1ในขณะที่มันอยู่ตรงข้ามสำหรับและT2 T2'ในภาษาอังกฤษสามารถอ่านได้ดังต่อไปนี้:

ฟังก์ชั่นเป็นชนิดย่อยของฟังก์ชั่นอื่นBถ้าพารามิเตอร์ชนิดของเป็น supertype ประเภทพารามิเตอร์ของBในขณะที่ประเภทการกลับมาของเป็นชนิดย่อยของประเภทกลับของB

เหตุผลสำหรับกฎนี้เหลือไว้สำหรับการออกกำลังกายให้กับผู้อ่าน (คำใบ้: คิดเกี่ยวกับกรณีที่แตกต่างกันเนื่องจากฟังก์ชั่นจะถูกพิมพ์ย่อยเช่นเดียวกับตัวอย่างอาร์เรย์ของฉันจากด้านบน)

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

trait List[+A] {
  def cons(hd: A): List[A]
}

ปัญหาคือว่าAเป็น covariant ในขณะที่consฟังก์ชั่นคาดว่าพารามิเตอร์ชนิดที่จะเป็นค่าคงที่ ดังนั้นการAเปลี่ยนแปลงทิศทางที่ผิด ที่น่าสนใจพอที่เราจะแก้ปัญหานี้โดยการListcontravariant ในAแต่แล้วพิมพ์กลับList[A]จะเป็นที่ไม่ถูกต้องในขณะที่consฟังก์ชั่นคาดว่าผลตอบแทนประเภทให้เป็นcovariant

เพียงสองตัวเลือกของเราที่นี่คือ a) ทำให้Aค่าคงที่สูญเสียคุณสมบัติการพิมพ์ย่อยที่ดีและใช้งานง่ายของความแปรปรวนร่วมหรือ b) เพิ่มพารามิเตอร์ประเภทท้องถิ่นลงในconsวิธีที่กำหนดAเป็นขอบเขตล่าง:

def cons[B >: A](v: B): List[B]

ตอนนี้ใช้ได้ คุณสามารถจินตนาการได้ว่าการAเปลี่ยนแปลงนั้นลดลง แต่Bสามารถแปรผันขึ้นไปได้ด้วยความเคารพAเนื่องจากAมันเป็นขอบเขตล่าง ด้วยการประกาศวิธีการนี้เราจะมีAความแปรปรวนร่วมและทุกอย่างได้ผล

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


4
สิ่งนี้สามารถระบุเป็นภาษาอังกฤษแบบธรรมดาได้ - คุณสามารถใช้อะไรที่ง่ายกว่าเป็นพารามิเตอร์และคุณสามารถคืนสิ่งที่ซับซ้อนกว่านี้ได้ไหม?
Phil

1
คอมไพเลอร์ Java (1.7.0) ไม่คอมไพล์ "Object [] arr = new int [1];" แต่ให้ข้อความแสดงข้อผิดพลาด: "java: ประเภทที่เข้ากันไม่ได้ต้องการ: java.lang.Object [] พบ: int []" ฉันคิดว่าคุณหมายถึง "Object [] arr = new Integer [1];"
Emre Sevinç

2
เมื่อคุณพูดถึง "เหตุผลของกฎนี้ถูกใช้เป็นแบบฝึกหัดสำหรับผู้อ่าน (คำใบ้: คิดถึงกรณีที่แตกต่างกันเนื่องจากฟังก์ชั่นจะถูกพิมพ์ย่อยเช่นตัวอย่างอาร์เรย์ของฉันจากด้านบน)" คุณสามารถยกตัวอย่างจริง ๆ ได้ไหม
perryzheng

2
@perryzheng ต่อนี้ใช้เวลาtrait Animal, trait Cow extends Animal, และdef iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c) def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a)จากนั้นiNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})ก็โอเคเพราะสัตว์เลี้ยงของเราสามารถเลี้ยงวัว แต่ iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})ให้ข้อผิดพลาดในการรวบรวมเนื่องจากสัตว์เลี้ยงวัวของเราไม่สามารถเลี้ยงสัตว์ทั้งหมดได้
Lasf

สิ่งนี้เกี่ยวข้องและช่วยฉันด้วยความแปรปรวน: typelevel.org/blog/2016/02/04/variance-and-functors.html
Peter Schmitz

27

@Daniel ได้อธิบายไว้เป็นอย่างดี แต่จะอธิบายโดยย่อหากได้รับอนุญาต:

  class Slot[+T](var some: T) {
    def get: T = some   
  }

  val slot: Slot[Dog] = new Slot[Dog](new Dog)   
  val slot2: Slot[Animal] = slot  //because of co-variance 
  slot2.some = new Animal   //legal as some is a var
  slot.get ??

slot.getแล้วจะโยนความผิดพลาดที่รันไทม์ที่มันเป็นประสบความสำเร็จในการแปลงAnimalไปDog(duh!)

โดยทั่วไปความแปรปรวนไม่เป็นไปด้วยดีกับการแปรปรวนร่วมและการแปรปรวนตรงกันข้าม นั่นคือเหตุผลว่าทำไมคอลเลกชัน Java ทั้งหมดจึงไม่เปลี่ยนแปลง


7

ดูตัวอย่างของ Scalaหน้า 57+ สำหรับการสนทนาแบบเต็ม

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

val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x             // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2))        // Works, but now x.get() will blow up 

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

เพื่อตอบสนองต่อการแก้ไขของคุณ: ช่องที่ไม่เปลี่ยนรูปเป็นสถานการณ์ที่แตกต่างกันโดยสิ้นเชิง ... * ยิ้ม * ฉันหวังว่าตัวอย่างข้างต้นจะช่วยได้


ฉันได้อ่านแล้ว; น่าเสียดายที่ฉัน (ยัง) ไม่เข้าใจว่าฉันสามารถทำสิ่งที่ฉันถามข้างต้น (เช่นจริง ๆ เขียน covariant ชั้นเรียน parametrized ใน T)
432923 oxbow_lakes

ฉันลบเครื่องหมายดาวน์เนื่องจากฉันรู้ว่านี่ค่อนข้างรุนแรง ฉันควรทำให้ชัดเจนในคำถามที่ฉันได้อ่านบิตจาก Scala ตามตัวอย่าง; ฉันแค่อยากให้มันอธิบายในลักษณะ "ไม่เป็นทางการ"
oxbow_lakes

@oxbow_lakes ยิ้มฉันกลัว Scala โดยตัวอย่างคือคำอธิบายที่เป็นทางการน้อยกว่า ที่ดีที่สุดที่เราสามารถพยายามที่จะใช้ตัวอย่างที่เป็นรูปธรรมในการทำงานแม้ว่ามันนี่ ...
MarkusQ

ขออภัย - ฉันไม่ต้องการให้ช่องของฉันเปลี่ยนแปลงไม่ได้ ฉันเพิ่งรู้ว่าปัญหาคือฉันประกาศ var และไม่ใช่ val
oxbow_lakes

3

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

class Slot[+T, V <: T](var some: V) {
  //blah
}

ตัวอย่างสกาล่าเป็นเรื่องยากที่จะเข้าใจตัวอย่างที่เป็นรูปธรรมจะช่วยได้

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