โดยทั่วไปพารามิเตอร์ประเภท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 จะ "ช่วย" โยนArrayStoreException
at 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
เปลี่ยนแปลงทิศทางที่ผิด ที่น่าสนใจพอที่เราจะแก้ปัญหานี้โดยการList
contravariant ใน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
ซึ่งไม่ได้รับอนุญาตจากคอมไพเลอร์ เมื่อใดก็ตามที่คุณมีความไม่แน่นอนคุณจำเป็นต้องมีตัวแปลบางอย่างซึ่งต้องใช้พารามิเตอร์เมธอดบางประเภทซึ่ง (รวมถึงตัวเข้าถึง) แสดงถึงความไม่แปรเปลี่ยน ความแปรปรวนร่วมทำงานกับข้อมูลที่เปลี่ยนแปลงไม่ได้เนื่องจากการดำเนินการที่เป็นไปได้เพียงอย่างเดียวคือการเข้าถึงซึ่งอาจได้รับประเภทผลตอบแทนแปรปรวนร่วม
var
เป็นตัวอย่างในขณะที่val
ไม่ได้ มันเป็นเหตุผลเดียวกันกับที่คอลเลคชั่นที่ไม่เปลี่ยนรูปแบบของสกาล่านั้นแปรเปลี่ยนไป แต่สิ่งที่เปลี่ยนแปลงไม่ได้