0Pricing
Kotlin Academy · บทเรียน

ความแปรผัน ณ จุดประกาศ: in และ out

ใช้พารามิเตอร์ชนิดแบบโคแวเรียนต์ (out) และคอนทราแวเรียนต์ (in) อย่างถูกต้อง

ความแปรผัน ณ จุดประกาศ: in และ out เป็นบทเรียน Kotlin Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Kotlin Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Kotlin Academy มีบทเรียนทั้งหมด 4 บทเรียน

ปัญหาความแปรผัน

ใน Kotlin List<String> เป็นชนิดย่อยของ List<Any> เพราะ List ประกาศด้วย out T หากไม่มีคำกำกับความแปรผัน ความสัมพันธ์นี้จะไม่เกิดขึ้น

val strings: List<String> = listOf("a", "b")
val anys: List<Any> = strings  // OK because List<out T>
// MutableList<String> is NOT a subtype of MutableList<Any>:
// val m: MutableList<Any> = mutableListOf("x") // compile error
fun main() { println(anys) }

ความแปรผันร่วมด้วย out

out T หมายความว่าคลาสสามารถ สร้าง ค่า T ได้เท่านั้น (ส่งคืนค่า) และไม่สามารถรับค่าเหล่านั้นได้ ทำให้ Producer<Dog> เป็นชนิดย่อยของ Producer<Animal>

interface Producer<out T> {
    fun produce(): T
}
class DogProducer : Producer<String> {
    override fun produce() = "Woof!"
}
fun sound(p: Producer<Any>) = println(p.produce())
fun main() {
    val dog = DogProducer()
    sound(dog)  // OK: Producer<String> is a subtype of Producer<Any>
}

ความแปรผันตรงข้ามด้วย in

in T หมายความว่าคลาสสามารถ รับ ค่า T ได้เท่านั้น (รับเป็นพารามิเตอร์) และไม่สามารถสร้างค่าเหล่านั้นได้ ทำให้ Consumer<Animal> เป็นชนิดย่อยของ Consumer<Dog>

interface Consumer<in T> {
    fun consume(item: T)
}
class Printer : Consumer<Any> {
    override fun consume(item: Any) = println(item)
}
fun feedDog(consumer: Consumer<String>) = consumer.consume("Dog")
fun main() {
    val printer = Printer()
    feedDog(printer)  // OK: Consumer<Any> is a subtype of Consumer<String>
}

ข้อจำกัดของ out: ห้ามใช้ในตำแหน่งรับค่า

เมื่อใช้ out T จะไม่สามารถใช้ T ในตำแหน่งพารามิเตอร์ (in) ได้ คอมไพเลอร์จะบังคับใช้ข้อจำกัดนี้

interface ReadOnly<out T> {
    fun get(): T          // OK: out position
    // fun set(t: T) {}  // Error: T in in-position violates out variance
}
class Box<out T>(private val value: T) : ReadOnly<T> {
    override fun get() = value
}
fun main() {
    val box: ReadOnly<Any> = Box<String>("hello")
    println(box.get())
}

ข้อจำกัดของ in: ห้ามใช้ในตำแหน่งส่งค่า

เมื่อใช้ in T จะไม่สามารถใช้ T ในตำแหน่งผลลัพธ์ (out) ได้

interface WriteOnly<in T> {
    fun set(t: T)        // OK: in position
    // fun get(): T {}  // Error: T in out-position violates in variance
}
class Sink<in T> : WriteOnly<T> {
    override fun set(t: T) = println("Received: $t")
}
fun main() {
    val sink: WriteOnly<String> = Sink<Any>()
    sink.set("hello")
}

ตัวอย่างจากไลบรารีมาตรฐานของ Kotlin

Comparable<in T> มีความแปรผันตรงข้าม: Comparable<Number> สามารถเปรียบเทียบชนิดย่อยของ Number ได้ทุกชนิด ส่วน List<out T> มีความแปรผันร่วม: รายการแมวถือเป็นรายการสัตว์ได้

fun sortNumbers(list: List<Number>) = list.sortedWith(compareBy { it.toDouble() })
fun main() {
    val ints: List<Int> = listOf(3, 1, 2)
    val sorted = sortNumbers(ints)  // OK: List<Int> is List<Number>
    println(sorted)
}

การไม่แปรผัน: MutableList

MutableList<T> ไม่แปรผัน เพราะสามารถทั้งสร้างและรับค่า T ได้ จึงไม่มีความสัมพันธ์แบบชนิดย่อยระหว่างชนิดข้อมูล T ที่แตกต่างกัน

fun addNumber(list: MutableList<Number>) { list.add(1.5) }
fun main() {
    val ints = mutableListOf<Int>(1, 2)
    // addNumber(ints) // compile error: MutableList<Int> is not MutableList<Number>
    val nums = mutableListOf<Number>(1, 2)
    addNumber(nums)   // OK
    println(nums)
}

ความแปรผัน ณ จุดใช้งานเป็นทางเลือก

เมื่อไม่สามารถเปลี่ยนคลาสได้ ให้ใช้ความแปรผัน ณ จุดใช้งาน โดยใช้ out ที่จุดเรียกใช้เพื่อฉายชนิดข้อมูลให้มีความแปรผันร่วม

fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
    for (item in from) to.add(item)
}
fun main() {
    val src = mutableListOf("a", "b", "c")
    val dest = mutableListOf<Any>()
    copy(src, dest)
    println(dest)
}

ภาคปฏิบัติ: รูปแบบ Repository

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

interface Repository<out T> {
    fun findAll(): List<T>
    fun findById(id: Int): T?
}
interface Writer<in T> {
    fun save(entity: T)
}
interface ReadWriteRepo<T> : Repository<T>, Writer<T>
// ReadWriteRepo<User> is neither sub nor super of ReadWriteRepo<Admin>

แนวทางเลือกความแปรผัน

เลือกความแปรผันตามวิธีใช้ T: out = ตัวสร้าง (ส่งคืน T เท่านั้น), in = ตัวรับ (รับ T เท่านั้น), ไม่แปรผัน = ทำได้ทั้งสองอย่าง (MutableList, ช่องสัญญาณ)

// Quick mental model:
// out T: source/producer  — List, Flow, Sequence
// in  T: sink/consumer    — Comparable, Continuation
// invariant: read+write   — MutableList, Channel, MutableStateFlow
fun main() {
    val nums: List<Number> = listOf(1, 2, 3)  // List is out
    println(nums)
}

คลาสที่ไม่แปรผันพร้อมฟังก์ชัน out

แม้ในคลาสที่ไม่แปรผัน คุณก็ยังใช้ out ณ จุดใช้งานกับพารามิเตอร์ฟังก์ชันเฉพาะได้

class Stack<T>(private val items: MutableList<T> = mutableListOf()) {
    fun push(item: T) = items.add(item)
    fun pop(): T? = items.removeLastOrNull()
}
fun printAll(stack: Stack<out Any>) {
    // can only read, not write
    println(stack.pop())
}
fun main() {
    val s = Stack<String>()
    s.push("hello")
    printAll(s)
}

ตรวจสอบความเข้าใจ

out T บนพารามิเตอร์ชนิดข้อมูลหมายความว่าอย่างไร

สรุปทบทวน

out T (ความแปรผันร่วม) อนุญาตให้เกิดความสัมพันธ์แบบชนิดย่อยเมื่อมีการสร้างค่า in T (ความแปรผันตรงข้าม) อนุญาตให้เกิดความสัมพันธ์แบบชนิดย่อยเมื่อมีการรับค่า ชนิดข้อมูลที่ไม่แปรผัน (ทั้งอ่านและเขียน) ไม่มีความสัมพันธ์แบบชนิดย่อยระหว่าง T ที่แตกต่างกัน

คำถามที่พบบ่อย

บทเรียน “ความแปรผัน ณ จุดประกาศ: in และ out” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ความแปรผัน ณ จุดประกาศ: in และ out” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Kotlin Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Kotlin Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ความแปรผัน ณ จุดประกาศ: in และ out”

ใช้พารามิเตอร์ชนิดแบบโคแวเรียนต์ (out) และคอนทราแวเรียนต์ (in) อย่างถูกต้อง คุณปฏิบัติ Kotlin Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Kotlin Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Kotlin Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “ความแปรผัน ณ จุดประกาศ: in และ out” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Kotlin Academy นี้ได้ไหม

ได้ บทเรียน Kotlin Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

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

  1. ฟังก์ชัน Generic และข้อจำกัดชนิดด้วย where
  2. ความแปรผัน ณ จุดประกาศ: in และ out
  3. Star Projection และการใช้ *
  4. การลบชนิดและพารามิเตอร์ชนิด reified
← กลับไปที่ Kotlin Academy