ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์
เชี่ยวชาญโคแวเรียนซ์และคอนทราแวเรียนซ์ เพื่อจัดการความสัมพันธ์ของชนิดย่อยในชนิดข้อมูลแบบเจเนริกได้อย่างถูกต้อง
ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์ เป็นบทเรียน Scala for Backend Engineering & Functional Programming ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 3 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Scala for Backend Engineering & Functional Programming และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 3 บทเรียน
ความแปรผันของชนิดข้อมูลคืออะไร
ใน Scala เมื่อคุณมีลำดับชั้นของชนิดข้อมูล (เช่น Cat เป็นชนิดย่อยของ Animal) ชนิดข้อมูลแบบเจเนริกจะมีพฤติกรรมอย่างไร
List[Cat] ถือเป็นชนิดย่อยของ List[Animal] หรือไม่ คำตอบคือไม่เสมอไปตามค่าเริ่มต้น
ความแปรผันของชนิดข้อมูล ช่วยให้เรากำหนดได้ว่าความสัมพันธ์ของชนิดย่อยจะคงเดิม (หรือกลับทิศทาง) อย่างไรสำหรับชนิดข้อมูลแบบเจเนริก ซึ่งเป็นแนวคิดสำคัญสำหรับการเขียนโค้ดที่ยืดหยุ่นและปลอดภัยด้านชนิดข้อมูล
ความแปรผันแบบร่วม: ชนิดข้อมูลผู้ผลิต (+T)
ความแปรผันแบบร่วม ระบุโดยใส่ + ไว้หน้าพารามิเตอร์ชนิดข้อมูล (เช่น trait Box[+T])
- หาก
Aเป็นชนิดย่อยของBดังนั้นBox[A]จะกลายเป็นชนิดย่อยของBox[B] - ให้คิดว่าชนิดข้อมูลแบบร่วมเป็น "ผู้ผลิต" ของ
Tชนิดข้อมูลเหล่านี้ปรากฏได้เฉพาะใน ตำแหน่งผลลัพธ์ (เช่น ชนิดผลลัพธ์ของเมธอด) - ดังนั้น หากต้องการ
Box[Animal]ก็สามารถส่งBox[Cat]ให้ได้ เพราะมัน "ผลิต" สิ่งที่เฉพาะเจาะจงกว่า (โดยCatเป็นAnimal)
ความแปรผันแบบร่วมในการใช้งานจริง
ลองเรียกใช้ตัวอย่างเทรต Producer ที่มีความแปรผันแบบร่วมนี้:
class Animal
class Cat extends Animal
trait Producer[+T] {
def produce: T // Output position
}
class CatProducer extends Producer[Cat] {
def produce: Cat = new Cat
}
object Main {
def main(args: Array[String]): Unit = {
val catProducer: Producer[Cat] = new CatProducer
// Because Producer is covariant, Producer[Cat] is a subtype of Producer[Animal]
val animalProducer: Producer[Animal] = catProducer
println("Assigned CatProducer to AnimalProducer.")
println(s"Produced: ${animalProducer.produce.getClass.getSimpleName}")
}
}ควรใช้ความแปรผันแบบร่วมเมื่อใด
ความแปรผันแบบร่วมปลอดภัยเมื่อชนิดข้อมูลแบบเจเนริกของคุณทำหน้าที่ "ผลิต" ค่า T เท่านั้น หรือไม่เคยรับ T เป็นอาร์กิวเมนต์
- คอลเลกชันที่เปลี่ยนแปลงไม่ได้ เช่น
List[+T]เป็นตัวอย่างที่ชัดเจน คุณสามารถมองList[Cat]เป็นList[Animal]ได้ เพราะคุณทำได้เพียง "อ่าน"Animal(หรือชนิดย่อยของมัน) จากรายการ - คุณไม่สามารถเพิ่ม
Animalใด ๆ ลงในList[Cat]หากรายการนั้นถูกระบุชนิดเป็นList[Animal]ซึ่งช่วยรักษาความปลอดภัยของชนิดข้อมูล
ความแปรผันแบบตรงข้าม: ชนิดข้อมูลผู้บริโภค (-T)
ความแปรผันแบบตรงข้าม ระบุโดยใส่ - ไว้หน้าพารามิเตอร์ชนิดข้อมูล (เช่น trait Consumer[-T])
- หาก
Aเป็นชนิดย่อยของBดังนั้นConsumer[B]จะกลายเป็นชนิดย่อยของConsumer[A] - ให้คิดว่าชนิดข้อมูลแบบตรงข้ามเป็น "ผู้บริโภค" ของ
Tชนิดข้อมูลเหล่านี้ปรากฏได้เฉพาะใน ตำแหน่งอินพุต (เช่น ชนิดพารามิเตอร์ของเมธอด) - ดังนั้น หากต้องการ
Consumer[Cat]ก็สามารถส่งConsumer[Animal]ให้ได้ เพราะมัน "บริโภค" สิ่งที่ทั่วไปกว่า (สามารถจัดการAnimalใด ๆ รวมถึงCatได้)
ความแปรผันแบบตรงข้ามในการใช้งานจริง
ลองเรียกใช้ตัวอย่างเทรต Consumer ที่มีความแปรผันแบบตรงข้ามนี้:
class Animal
class Cat extends Animal
trait Consumer[-T] {
def consume(item: T): Unit // Input position
}
class AnimalConsumer extends Consumer[Animal] {
def consume(item: Animal): Unit = {
println(s"Consumed an animal: ${item.getClass.getSimpleName}")
}
}
object Main {
def main(args: Array[String]): Unit = {
val animalConsumer: Consumer[Animal] = new AnimalConsumer
// Because Consumer is contravariant, Consumer[Animal] is a subtype of Consumer[Cat]
val catConsumer: Consumer[Cat] = animalConsumer
catConsumer.consume(new Cat)
println("Assigned AnimalConsumer to CatConsumer.")
}
}ควรใช้ความแปรผันแบบตรงข้ามเมื่อใด
ความแปรผันแบบตรงข้ามปลอดภัยเมื่อชนิดข้อมูลแบบเจเนริกของคุณทำหน้าที่ "บริโภค" ค่า T เท่านั้น หรือไม่เคยส่งคืน T
- ตัวอย่างทั่วไปคือ ฟังก์ชัน โดยเฉพาะชนิดของพารามิเตอร์อินพุต หากฟังก์ชันสามารถประมวลผล
Animalใด ๆ ได้ (Animal => Unit) ก็ย่อมประมวลผลCatได้แน่นอน ดังนั้น(Animal => Unit)จึงเป็นชนิดย่อยของ(Cat => Unit) - วิธีนี้ช่วยเพิ่มความยืดหยุ่นเมื่อต้องส่งฟังก์ชันเป็นอาร์กิวเมนต์
ความไม่แปรผัน: พฤติกรรมเริ่มต้น
หากคุณไม่ระบุ + หรือ - พารามิเตอร์ชนิดข้อมูลจะเป็นแบบ ไม่แปรผัน ซึ่งเป็นค่าเริ่มต้นใน Scala
Box[A]จะเป็นชนิดย่อยของBox[B]ก็ต่อเมื่อAเป็นชนิดเดียวกับBโดยตรง เท่านั้น- พฤติกรรมนี้มักจำเป็นสำหรับคอลเลกชันที่เปลี่ยนแปลงได้ (เช่น
Array[T]) เพื่อป้องกันปัญหาด้านความปลอดภัยของชนิดข้อมูล เนื่องจากคุณสามารถอ่านและเขียนชนิดย่อยที่แตกต่างกันได้
class Food
class Apple extends Food
// Invariant Box
class Box[T](val item: T) {
def getContent: T = item
}
object Main {
def main(args: Array[String]): Unit = {
val appleBox = new Box(new Apple)
// The following line would cause a compile error:
// val foodBox: Box[Food] = appleBox
println(s"An Apple Box contains: ${appleBox.getContent.getClass.getSimpleName}")
println("Box[Apple] is NOT a subtype of Box[Food] (invariant).")
println("The types must match exactly for invariant types.")
}
}ฟังก์ชัน: ทั้งความแปรผันแบบร่วมและแบบตรงข้าม
ชนิดฟังก์ชันของ Scala อย่าง Function1[-A, +B] ผสานความแปรผันทั้งสองแบบได้อย่างลงตัว:
- พารามิเตอร์อินพุต
Aเป็น ความแปรผันแบบตรงข้าม (-A) หมายความว่าฟังก์ชันที่รับชนิดข้อมูลทั่วไปกว่า (เช่นAnimal) สามารถใช้แทนฟังก์ชันที่รับชนิดข้อมูลเฉพาะเจาะจงกว่า (เช่นCat) ได้ - ชนิดผลลัพธ์
Bเป็น ความแปรผันแบบร่วม (+B) หมายความว่าฟังก์ชันที่ส่งคืนชนิดข้อมูลเฉพาะเจาะจงกว่า (เช่นCat) สามารถใช้แทนฟังก์ชันที่ส่งคืนชนิดข้อมูลทั่วไปกว่า (เช่นAnimal) ได้
class Vehicle
class Car extends Vehicle
object Main {
def main(args: Array[String]): Unit = {
// Contravariance for input: (Vehicle => Unit) is a subtype of (Car => Unit)
val printVehicle: Vehicle => Unit = (v: Vehicle) => println(s"Printing vehicle: ${v.getClass.getSimpleName}")
val printCar: Car => Unit = printVehicle // OK: A general printer can print a specific car
printCar(new Car)
// Covariance for output: (() => Car) is a subtype of (() => Vehicle)
val getCar: () => Car = () => new Car
val getVehicle: () => Vehicle = getCar // OK: A specific producer can fulfill a general request
println(s"Got vehicle: ${getVehicle().getClass.getSimpleName}")
}
}ตรวจสอบสั้น ๆ: กฎความแปรผัน
พิจารณาเทรตต่อไปนี้:
trait Handler[T] {
def handle(item: T): Unit
}หากต้องการให้ Handler[Animal] ใช้แทนที่ Handler[Cat] ได้ (โดย Cat สืบทอดจาก Animal) ควรใช้คำกำกับความแปรผันแบบใดกับ T
สรุป: เชี่ยวชาญความแปรผัน
คุณเข้าใจความแปรผันใน Scala ได้อย่างยอดเยี่ยมแล้วครับ มาทบทวนอย่างรวดเร็วกัน:
- ความแปรผันแบบร่วม (
+T): ทำให้Container[Subtype]เป็นชนิดย่อยของContainer[Supertype]เหมาะสำหรับชนิดข้อมูล "ผู้ผลิต" ที่ส่งคืนTเท่านั้น - ความแปรผันแบบตรงข้าม (
-T): ทำให้Container[Supertype]เป็นชนิดย่อยของContainer[Subtype]เหมาะสำหรับชนิดข้อมูล "ผู้บริโภค" ที่รับTเป็นอินพุตเท่านั้น - ความไม่แปรผัน: เป็นค่าเริ่มต้น ชนิดข้อมูลต้องตรงกันทุกประการ
การเข้าใจความแปรผันช่วยให้คุณสร้างโค้ดแบบเจเนริกใน Scala ที่ยืดหยุ่นและปลอดภัยด้านชนิดข้อมูลมากขึ้น
คำถามที่พบบ่อย
บทเรียน “ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Scala for Backend Engineering & Functional Programming ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Scala for Backend Engineering & Functional Programming มีบทเรียนทั้งหมด 3 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์”
เชี่ยวชาญโคแวเรียนซ์และคอนทราแวเรียนซ์ เพื่อจัดการความสัมพันธ์ของชนิดย่อยในชนิดข้อมูลแบบเจเนริกได้อย่างถูกต้อง คุณปฏิบัติ Scala for Backend Engineering & Functional Programming ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Scala for Backend Engineering & Functional Programming หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Scala for Backend Engineering & Functional Programming บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 3 บทเรียน
บทเรียน “ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Scala for Backend Engineering & Functional Programming นี้ได้ไหม
ได้ บทเรียน Scala for Backend Engineering & Functional Programming ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เจเนริกและพารามิเตอร์ชนิดข้อมูล
- ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์
- คลาสชนิดข้อมูลและอิมพลิซิต