0Pricing
Scala for Backend Engineering & Functional Programming · บทเรียน

ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์

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

ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์ เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. เจเนริกและพารามิเตอร์ชนิดข้อมูล
  2. ความแปรผัน: โคแวเรียนซ์และคอนทราแวเรียนซ์
  3. คลาสชนิดข้อมูลและอิมพลิซิต
← กลับไปที่ Scala for Backend Engineering & Functional Programming