Kotlin Bootcamp สำหรับโปรแกรมเมอร์ 5.2: Generics

Codelab นี้เป็นส่วนหนึ่งของหลักสูตร Kotlin Bootcamp สำหรับโปรแกรมเมอร์ คุณจะได้รับประโยชน์สูงสุดจากหลักสูตรนี้หากทำตาม Codelab ตามลำดับ คุณอาจข้ามบางส่วนได้ ทั้งนี้ขึ้นอยู่กับความรู้ของคุณ หลักสูตรนี้เหมาะสำหรับโปรแกรมเมอร์ที่รู้จักภาษาเชิงวัตถุและต้องการเรียนรู้ Kotlin

บทนำ

ในโค้ดแล็บนี้ คุณจะได้รู้จักคลาส ฟังก์ชัน และเมธอดทั่วไป รวมถึงวิธีการทำงานใน Kotlin

บทเรียนในหลักสูตรนี้ได้รับการออกแบบมาเพื่อสร้างความรู้ของคุณ แต่จะมีความเป็นอิสระจากกันในระดับหนึ่งเพื่อให้คุณข้ามส่วนที่คุณคุ้นเคยได้ แทนที่จะสร้างแอปตัวอย่างเพียงแอปเดียว ตัวอย่างหลายรายการใช้ธีมตู้ปลาเพื่อเชื่อมโยงตัวอย่างต่างๆ เข้าด้วยกัน และหากต้องการดูเรื่องราวทั้งหมดของตู้ปลา ให้ดูหลักสูตร Kotlin Bootcamp for Programmers ของ Udacity

สิ่งที่คุณควรทราบอยู่แล้ว

  • ไวยากรณ์ของฟังก์ชัน คลาส และเมธอด Kotlin
  • วิธีสร้างคลาสใหม่ใน IntelliJ IDEA และเรียกใช้โปรแกรม

สิ่งที่คุณจะได้เรียนรู้

  • วิธีใช้คลาส เมธอด และฟังก์ชันทั่วไป

สิ่งที่คุณต้องดำเนินการ

  • สร้างคลาสทั่วไปและเพิ่มข้อจำกัด
  • สร้างประเภท in และ out
  • สร้างฟังก์ชัน เมธอด และฟังก์ชันส่วนขยายทั่วไป

ข้อมูลเบื้องต้นเกี่ยวกับ Generics

Kotlin มีประเภททั่วไปเช่นเดียวกับภาษาโปรแกรมอื่นๆ ประเภททั่วไปช่วยให้คุณสร้างคลาสทั่วไปได้ ซึ่งจะทำให้คลาสมีความยืดหยุ่นมากขึ้น

สมมติว่าคุณกำลังใช้MyListคลาสที่เก็บรายการสินค้า หากไม่มี Generics คุณจะต้องใช้ MyList เวอร์ชันใหม่สำหรับแต่ละประเภท ได้แก่ เวอร์ชันหนึ่งสำหรับ Double, เวอร์ชันหนึ่งสำหรับ String และเวอร์ชันหนึ่งสำหรับ Fish โดยใช้ Generics คุณจะทำให้ลิสต์เป็นแบบทั่วไปได้ เพื่อให้สามารถเก็บออบเจ็กต์ประเภทใดก็ได้ ซึ่งก็เหมือนกับการทำให้ประเภทเป็นไวลด์การ์ดที่เหมาะกับหลายประเภท

หากต้องการกำหนดประเภททั่วไป ให้ใส่ T ในวงเล็บมุม <T> หลังชื่อคลาส (คุณอาจใช้อักษรอื่นหรือชื่อที่ยาวกว่านี้ได้ แต่รูปแบบสำหรับประเภททั่วไปคือ T)

class MyList<T> {
    fun get(pos: Int): T {
        TODO("implement")
    }
    fun addItem(item: T) {}
}

คุณอ้างอิง T ได้ราวกับว่าเป็นประเภทปกติ ประเภทการคืนค่าสำหรับ get() คือ T และพารามิเตอร์สำหรับ addItem() มีประเภทเป็น T แน่นอนว่าลิสต์ทั่วไปมีประโยชน์มาก ดังนั้น List คลาสจึงสร้างขึ้นใน Kotlin

ขั้นตอนที่ 1: สร้างลำดับชั้นของประเภท

ในขั้นตอนนี้ คุณจะได้สร้างคลาสบางคลาสเพื่อใช้ในขั้นตอนถัดไป เราได้พูดถึงการสร้างคลาสย่อยใน Codelab ก่อนหน้านี้แล้ว แต่จะมาทบทวนสั้นๆ กันอีกครั้ง

  1. เพื่อไม่ให้ตัวอย่างรก ให้สร้างแพ็กเกจใหม่ใน src แล้วตั้งชื่อว่า generics
  2. สร้างไฟล์ Aquarium.kt ใหม่ในแพ็กเกจ generics ซึ่งจะช่วยให้คุณกำหนดสิ่งต่างๆ ใหม่ได้โดยใช้ชื่อเดียวกันโดยไม่มีข้อขัดแย้ง ดังนั้นโค้ดที่เหลือของ Codelab นี้จะอยู่ในไฟล์นี้
  3. สร้างลำดับชั้นประเภทของประเภทการจ่ายน้ำ เริ่มต้นด้วยการสร้าง WaterSupply เป็นคลาส open เพื่อให้สามารถสร้างคลาสย่อยได้
  4. เพิ่มvarพารามิเตอร์needsProcessingบูลีน การดำเนินการนี้จะสร้างพร็อพเพอร์ตี้ที่เปลี่ยนแปลงได้โดยอัตโนมัติ พร้อมด้วย Getter และ Setter
  5. สร้างคลาสย่อย TapWater ที่ขยาย WaterSupply และส่ง true สำหรับ needsProcessing เนื่องจากน้ำประปามีสารเติมแต่งซึ่งไม่ดีต่อปลา
  6. ใน TapWater ให้กำหนดฟังก์ชันที่ชื่อ addChemicalCleaners() ซึ่งตั้งค่า needsProcessing เป็น false หลังจากทำความสะอาดน้ำ ตั้งค่าพร็อพเพอร์ตี้ needsProcessing จาก TapWater ได้เนื่องจากเป็น public โดยค่าเริ่มต้นและเข้าถึงได้สำหรับคลาสย่อย โค้ดที่เสร็จสมบูรณ์แล้วมีดังนี้
package generics

open class WaterSupply(var needsProcessing: Boolean)

class TapWater : WaterSupply(true) {
   fun addChemicalCleaners() {
       needsProcessing = false
   }
}
  1. สร้างคลาสย่อยอีก 2 คลาสของ WaterSupply ชื่อ FishStoreWater และ LakeWater FishStoreWater ไม่จำเป็นต้องประมวลผล แต่ต้องกรอง LakeWater ด้วยวิธี filter() หลังจากกรองแล้ว ไม่จำเป็นต้องประมวลผลอีกครั้ง ดังนั้นใน filter() ให้ตั้งค่า needsProcessing = false
class FishStoreWater : WaterSupply(false)

class LakeWater : WaterSupply(true) {
   fun filter() {
       needsProcessing = false
   }
}

หากต้องการข้อมูลเพิ่มเติม โปรดดูบทเรียนก่อนหน้าเกี่ยวกับการรับค่าใน Kotlin

ขั้นตอนที่ 2: สร้างคลาสทั่วไป

ในขั้นตอนนี้ คุณจะแก้ไขคลาส Aquarium เพื่อรองรับแหล่งน้ำประเภทต่างๆ

  1. ใน Aquarium.kt ให้กำหนดคลาส Aquarium โดยมี <T> ในวงเล็บหลังชื่อคลาส
  2. เพิ่มพร็อพเพอร์ตี้ที่ไม่เปลี่ยนแปลง waterSupply ประเภท T ลงใน Aquarium
class Aquarium<T>(val waterSupply: T)
  1. เขียนฟังก์ชันที่ชื่อ genericsExample() ซึ่งไม่ได้เป็นส่วนหนึ่งของคลาส จึงสามารถวางไว้ที่ระดับบนสุดของไฟล์ได้ เช่น ฟังก์ชัน main() หรือคำจำกัดความของคลาส ในฟังก์ชัน ให้สร้าง Aquarium และส่ง WaterSupply ไปยังฟังก์ชัน เนื่องจากพารามิเตอร์ waterSupply เป็นพารามิเตอร์ทั่วไป คุณจึงต้องระบุประเภทในวงเล็บมุม <>
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
}
  1. ใน genericsExample() รหัสของคุณจะเข้าถึง waterSupply ของพิพิธภัณฑ์สัตว์น้ำได้ เนื่องจากเป็นประเภท TapWater คุณจึงเรียกใช้ addChemicalCleaners() ได้โดยไม่ต้องมีการแคสต์ประเภท
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. เมื่อสร้างออบเจ็กต์ Aquarium คุณสามารถนำวงเล็บมุมและข้อความที่อยู่ระหว่างวงเล็บออกได้เนื่องจาก Kotlin มีการอนุมานประเภท ดังนั้นจึงไม่จำเป็นต้องพูดว่า TapWater สองครั้งเมื่อสร้างอินสแตนซ์ ระบบจะอนุมานประเภทได้จากอาร์กิวเมนต์ของ Aquarium แต่จะยังคงสร้าง Aquarium ประเภท TapWater
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. หากต้องการดูว่าเกิดอะไรขึ้น ให้พิมพ์ needsProcessing ก่อนและหลังเรียกใช้ addChemicalCleaners() ฟังก์ชันที่เสร็จสมบูรณ์แล้วมีดังนี้
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
    aquarium.waterSupply.addChemicalCleaners()
    println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}
  1. เพิ่มฟังก์ชัน main() เพื่อเรียกใช้ genericsExample() จากนั้นเรียกใช้โปรแกรมและสังเกตผลลัพธ์
fun main() {
    genericsExample()
}
⇒ water needs processing: true
water needs processing: false

ขั้นตอนที่ 3: ระบุรายละเอียดเพิ่มเติม

คำว่า "ทั่วไป" หมายความว่าคุณส่งอะไรก็ได้เกือบทุกอย่าง ซึ่งบางครั้งก็เป็นปัญหา ในขั้นตอนนี้ คุณจะAquariumคลาสให้เฉพาะเจาะจงมากขึ้นเกี่ยวกับสิ่งที่คุณใส่ลงในคลาสได้

  1. ใน genericsExample() ให้สร้าง Aquarium โดยส่งสตริงสำหรับ waterSupply จากนั้นพิมพ์พร็อพเพอร์ตี้ waterSupply ของตู้ปลา
fun genericsExample() {
    val aquarium2 = Aquarium("string")
    println(aquarium2.waterSupply)
}
  1. เรียกใช้โปรแกรมและสังเกตผลลัพธ์
⇒ string

ผลลัพธ์คือสตริงที่คุณส่ง เนื่องจาก Aquarium ไม่ได้จำกัดประเภทของ T. คุณจึงส่งประเภทใดก็ได้ รวมถึง String

  1. ใน genericsExample() ให้สร้าง Aquarium อีกรายการหนึ่งโดยส่ง null สำหรับ waterSupply หาก waterSupply เป็นค่าว่าง ให้พิมพ์ "waterSupply is null"
fun genericsExample() {
    val aquarium3 = Aquarium(null)
    if (aquarium3.waterSupply == null) {
        println("waterSupply is null")
    }
}
  1. เรียกใช้โปรแกรมและสังเกตผลลัพธ์
⇒ waterSupply is null

ทำไมคุณจึงส่ง null ได้เมื่อสร้าง Aquarium ซึ่งเป็นไปได้เนื่องจากโดยค่าเริ่มต้น T หมายถึงประเภท Any? ที่อนุญาตให้เป็นค่าว่าง ซึ่งเป็นประเภทที่อยู่ด้านบนสุดของลำดับชั้นของประเภท ต่อไปนี้คือสิ่งที่เทียบเท่ากับสิ่งที่คุณพิมพ์ก่อนหน้านี้

class Aquarium<T: Any?>(val waterSupply: T)
  1. หากไม่ต้องการส่งผ่าน null ให้ระบุ T ประเภท Any อย่างชัดเจนโดยนำ ? ออกหลังจาก Any
class Aquarium<T: Any>(val waterSupply: T)

ในบริบทนี้ Any เรียกว่าข้อจำกัดทั่วไป ซึ่งหมายความว่าคุณส่งประเภทใดก็ได้สำหรับ T ตราบใดที่ไม่ได้ส่ง null

  1. สิ่งที่คุณต้องการจริงๆ คือการตรวจสอบว่าเฉพาะ WaterSupply (หรือคลาสย่อยของคลาสนี้) เท่านั้นที่ส่งผ่านสำหรับ T ได้ แทนที่ Any ด้วย WaterSupply เพื่อกำหนดข้อจำกัดทั่วไปที่เฉพาะเจาะจงมากขึ้น
class Aquarium<T: WaterSupply>(val waterSupply: T)

ขั้นตอนที่ 4: เพิ่มการตรวจสอบ

ในขั้นตอนนี้ คุณจะได้เรียนรู้ฟังก์ชัน check() เพื่อช่วยให้มั่นใจว่าโค้ดทำงานได้ตามที่คาดไว้ ฟังก์ชัน check() เป็นฟังก์ชันไลบรารีมาตรฐานใน Kotlin โดยจะทำหน้าที่เป็นการยืนยันและจะแสดง IllegalStateException หากอาร์กิวเมนต์ประเมินเป็น false

  1. เพิ่มaddWater() method ไปยังคลาส Aquarium เพื่อเติมน้ำ โดยมี check() ที่ช่วยให้คุณไม่ต้องประมวลผลน้ำก่อน
class Aquarium<T: WaterSupply>(val waterSupply: T) {
    fun addWater() {
        check(!waterSupply.needsProcessing) { "water supply needs processing first" }
        println("adding water from $waterSupply")
    }    
}

ในกรณีนี้ หาก needsProcessing เป็นจริง check() จะส่งข้อยกเว้น

  1. ใน genericsExample() ให้เพิ่มโค้ดเพื่อสร้าง Aquarium ด้วย LakeWater แล้วเติมน้ำลงไป
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.addWater()
}
  1. เรียกใช้โปรแกรม แล้วคุณจะได้รับข้อยกเว้น เนื่องจากต้องกรองน้ำก่อน
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
        at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
  1. กรองน้ำก่อนเติมลงใน Aquarium ตอนนี้เมื่อคุณเรียกใช้โปรแกรม จะไม่มีการโยนข้อยกเว้น
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.waterSupply.filter()
    aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60

ข้อมูลข้างต้นครอบคลุมพื้นฐานของ Generics งานต่อไปนี้จะครอบคลุมรายละเอียดเพิ่มเติม แต่แนวคิดที่สำคัญคือวิธีประกาศและใช้คลาสทั่วไปที่มีข้อจำกัดทั่วไป

ในงานนี้ คุณจะได้เรียนรู้เกี่ยวกับประเภทอินและเอาต์ด้วย Generics ประเภท in คือประเภทที่ส่งไปยังคลาสได้เท่านั้น แต่จะส่งกลับไม่ได้ outประเภทคือประเภทที่แสดงได้จากคลาสเท่านั้น

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

ประเภท in และ out เป็นคำสั่งสำหรับระบบประเภทของ Kotlin การอธิบายระบบประเภททั้งหมดอยู่นอกขอบเขตของบูทแคมป์นี้ (ค่อนข้างซับซ้อน) อย่างไรก็ตาม คอมไพเลอร์จะแจ้งประเภทที่ไม่ได้ทำเครื่องหมาย in และ out อย่างเหมาะสม คุณจึงต้องทราบเกี่ยวกับประเภทดังกล่าว

ขั้นตอนที่ 1: กำหนดประเภทการออก

  1. ในคลาส Aquarium ให้เปลี่ยน T: WaterSupply เป็นประเภท out
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    ...
}
  1. ในไฟล์เดียวกัน นอกคลาส ให้ประกาศฟังก์ชัน addItemTo() ที่คาดหวัง Aquarium ของ WaterSupply
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
  1. เรียกใช้ addItemTo() จาก genericsExample() แล้วเรียกใช้โปรแกรม
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    addItemTo(aquarium)
}
⇒ item added

Kotlin ช่วยให้มั่นใจได้ว่า addItemTo() จะไม่ทำสิ่งใดที่ไม่ปลอดภัยกับประเภท WaterSupply ทั่วไป เนื่องจากมีการประกาศเป็นประเภท out

  1. หากนำคีย์เวิร์ด out ออก คอมไพเลอร์จะแสดงข้อผิดพลาดเมื่อเรียกใช้ addItemTo() เนื่องจาก Kotlin ไม่สามารถรับประกันได้ว่าคุณจะไม่ทำสิ่งที่ไม่ปลอดภัยกับประเภท

ขั้นตอนที่ 2: กำหนดประเภทอิน

ประเภท in คล้ายกับประเภท out แต่ใช้สำหรับประเภททั่วไปที่ส่งไปยังฟังก์ชันเท่านั้น ไม่ได้ส่งคืน หากพยายามส่งคืนประเภท in คุณจะได้รับข้อผิดพลาดของคอมไพเลอร์ ในตัวอย่างนี้ คุณจะกำหนดinประเภทเป็นส่วนหนึ่งของอินเทอร์เฟซ

  1. ใน Aquarium.kt ให้กำหนดอินเทอร์เฟซ Cleaner ที่ใช้ T ทั่วไปซึ่งจำกัดไว้ที่ WaterSupply เนื่องจากใช้เป็นอาร์กิวเมนต์กับ clean() เท่านั้น คุณจึงทําให้เป็นพารามิเตอร์ in ได้
interface Cleaner<in T: WaterSupply> {
    fun clean(waterSupply: T)
}
  1. หากต้องการใช้อินเทอร์เฟซ Cleaner ให้สร้างคลาส TapWaterCleaner ที่ใช้ Cleaner สำหรับการทำความสะอาด TapWater โดยการเติมสารเคมี
class TapWaterCleaner : Cleaner<TapWater> {
    override fun clean(waterSupply: TapWater) =   waterSupply.addChemicalCleaners()
}
  1. ในAquarium ให้อัปเดตaddWater()เพื่อใช้Cleanerประเภท T และทำความสะอาดน้ำก่อนเติม
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    fun addWater(cleaner: Cleaner<T>) {
        if (waterSupply.needsProcessing) {
            cleaner.clean(waterSupply)
        }
        println("water added")
    }
}
  1. อัปเดตgenericsExample()ตัวอย่างโค้ดเพื่อสร้างTapWaterCleaner Aquariumที่มีTapWater แล้วเติมน้ำโดยใช้เครื่องมือทำความสะอาด และจะใช้โปรแกรมล้างข้อมูลตามความจำเป็น
fun genericsExample() {
    val cleaner = TapWaterCleaner()
    val aquarium = Aquarium(TapWater())
    aquarium.addWater(cleaner)
}

Kotlin จะใช้ข้อมูลประเภท in และ out เพื่อให้แน่ใจว่าโค้ดของคุณใช้ Generics อย่างปลอดภัย Out และ in จำได้ง่าย: out ประเภทสามารถส่งออกเป็นค่าที่ส่งคืนได้ ส่วนประเภท in สามารถส่งเข้าเป็นอาร์กิวเมนต์ได้

หากต้องการเจาะลึกปัญหาประเภทต่างๆ และวิธีแก้ปัญหา โปรดดูเอกสารประกอบซึ่งอธิบายเรื่องนี้อย่างละเอียด

ในงานนี้ คุณจะได้เรียนรู้เกี่ยวกับฟังก์ชันทั่วไปและเวลาที่ควรใช้ฟังก์ชันดังกล่าว โดยปกติแล้ว การสร้างฟังก์ชันทั่วไปเป็นความคิดที่ดีทุกครั้งที่ฟังก์ชันรับอาร์กิวเมนต์ของคลาสที่มีประเภททั่วไป

ขั้นตอนที่ 1: สร้างฟังก์ชันทั่วไป

  1. ใน generics/Aquarium.kt ให้สร้างฟังก์ชัน isWaterClean() ซึ่งรับ Aquarium คุณต้องระบุประเภททั่วไปของพารามิเตอร์ โดยมีตัวเลือกหนึ่งคือการใช้ WaterSupply
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
   println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}

แต่หมายความว่า Aquarium ต้องมีพารามิเตอร์ประเภท out เพื่อให้เรียกใช้ได้ บางครั้ง out หรือ in ก็จำกัดมากเกินไปเนื่องจากคุณต้องใช้ประเภทสำหรับทั้งอินพุตและเอาต์พุต คุณสามารถนำoutข้อกำหนดออกได้โดยทำให้ฟังก์ชันเป็นแบบทั่วไป

  1. หากต้องการทำให้ฟังก์ชันเป็นแบบทั่วไป ให้ใส่เครื่องหมายวงเล็บปีกกาหลังคีย์เวิร์ด fun โดยมีประเภททั่วไป T และข้อจำกัดใดๆ ในกรณีนี้คือ WaterSupply เปลี่ยน Aquarium ให้ถูกจำกัดโดย T แทนที่จะเป็น WaterSupply
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
   println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}

T เป็นพารามิเตอร์ประเภทของ isWaterClean() ที่ใช้เพื่อระบุประเภททั่วไปของพิพิธภัณฑ์สัตว์น้ำ รูปแบบนี้พบได้ทั่วไป และคุณควรใช้เวลาสักครู่เพื่อทำความเข้าใจ

  1. เรียกใช้ฟังก์ชัน isWaterClean() โดยระบุประเภทในวงเล็บมุมหลังชื่อฟังก์ชันและก่อนวงเล็บ
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean<TapWater>(aquarium)
}
  1. เนื่องจากการอนุมานประเภทจากอาร์กิวเมนต์ aquarium จึงไม่จำเป็นต้องระบุประเภท ดังนั้นให้นำออก เรียกใช้โปรแกรมและสังเกตเอาต์พุต
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean(aquarium)
}
⇒ aquarium water is clean: false

ขั้นตอนที่ 2: สร้างเมธอดทั่วไปที่มีประเภทที่ทำให้เป็นจริง

คุณยังใช้ฟังก์ชันทั่วไปกับเมธอดได้ด้วย แม้ในคลาสที่มีประเภททั่วไปของตัวเอง ในขั้นตอนนี้ คุณจะเพิ่มเมธอดทั่วไปลงใน Aquarium ซึ่งจะตรวจสอบว่ามีประเภทเป็น WaterSupply หรือไม่

  1. ในคลาส Aquarium ให้ประกาศเมธอด hasWaterSupplyOfType() ที่ใช้พารามิเตอร์ทั่วไป R (ใช้ T แล้ว) ซึ่งจำกัดไว้ที่ WaterSupply และส่งคืน true หาก waterSupply เป็นประเภท R ซึ่งคล้ายกับฟังก์ชันที่คุณประกาศไว้ก่อนหน้านี้ แต่จะอยู่ภายในคลาส Aquarium
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
  1. โปรดสังเกตว่า R สุดท้ายมีขีดเส้นใต้สีแดง วางเคอร์เซอร์เหนือข้อความเพื่อดูว่าข้อผิดพลาดคืออะไร
  2. หากต้องการตรวจสอบ is คุณต้องบอก Kotlin ว่าประเภทเป็น reified หรือเป็นประเภทจริง และสามารถใช้ในฟังก์ชันได้ โดยใส่ inline ไว้หน้าfunคีย์เวิร์ด และ reified ไว้หน้าประเภททั่วไป R
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R

เมื่อทำให้ประเภทเป็นรูปธรรมแล้ว คุณจะใช้ประเภทดังกล่าวได้เหมือนกับประเภทปกติ เนื่องจากเป็นประเภทจริงหลังจากอินไลน์ ซึ่งหมายความว่าคุณสามารถisตรวจสอบโดยใช้ประเภทได้

หากคุณไม่ใช้ reified ที่นี่ ประเภทจะไม่ "สมจริง" พอที่ Kotlin จะอนุญาตการตรวจสอบ is เนื่องจากประเภทที่ไม่ได้รับการทำให้เป็นจริงจะใช้ได้เฉพาะในเวลาคอมไพล์ และโปรแกรมของคุณจะใช้ประเภทดังกล่าวในเวลาเรียกใช้ไม่ได้ เราจะพูดถึงเรื่องนี้เพิ่มเติมในส่วนถัดไป

  1. ส่ง TapWater เป็นประเภท เช่นเดียวกับการเรียกฟังก์ชันทั่วไป ให้เรียกเมธอดทั่วไปโดยใช้วงเล็บปีกกาที่มีประเภทอยู่หลังชื่อฟังก์ชัน เรียกใช้โปรแกรมและสังเกตผลลัพธ์
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.hasWaterSupplyOfType<TapWater>())   // true
}
⇒ true

ขั้นตอนที่ 3: สร้างฟังก์ชันส่วนขยาย

คุณยังใช้ประเภทที่ทำให้เป็นจริงสำหรับฟังก์ชันปกติและฟังก์ชันส่วนขยายได้ด้วย

  1. นอกคลาส Aquarium ให้กำหนดฟังก์ชันส่วนขยายใน WaterSupply ที่ชื่อ isOfType() ซึ่งจะตรวจสอบว่า WaterSupply ที่ส่งผ่านเป็นประเภทที่เฉพาะเจาะจงหรือไม่ เช่น TapWater
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
  1. เรียกใช้ฟังก์ชันส่วนขยายเหมือนกับเมธอด
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.waterSupply.isOfType<TapWater>())  
}
⇒ true

ฟังก์ชันส่วนขยายเหล่านี้ไม่ว่าจะเป็น Aquarium ประเภทใด (Aquarium หรือ TowerTank หรือคลาสย่อยอื่นๆ) ก็ใช้ได้ตราบใดที่เป็น Aquarium การใช้ไวยากรณ์การฉายภาพดาวเป็นวิธีที่สะดวกในการระบุการจับคู่ที่หลากหลาย และเมื่อคุณใช้การฉายภาพแบบสตาร์ Kotlin จะช่วยให้คุณไม่ทำสิ่งที่ไม่ปลอดภัยด้วย

  1. หากต้องการใช้การฉายดาว ให้ใส่ <*> หลัง Aquarium ย้าย hasWaterSupplyOfType() ไปเป็นฟังก์ชันส่วนขยาย เนื่องจากไม่ได้เป็นส่วนหนึ่งของ API หลักของ Aquarium
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
  1. เปลี่ยนการเรียกใช้เป็น hasWaterSupplyOfType() แล้วเรียกใช้โปรแกรม
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true

ในตัวอย่างก่อนหน้านี้ คุณต้องทำเครื่องหมายประเภททั่วไปเป็น reified และทำให้ฟังก์ชันเป็น inline เนื่องจาก Kotlin ต้องทราบเกี่ยวกับประเภททั่วไปและฟังก์ชันที่รันไทม์ ไม่ใช่แค่ที่คอมไพล์ไทม์

Kotlin จะใช้ประเภททั่วไปทั้งหมดในเวลาคอมไพล์เท่านั้น ซึ่งจะช่วยให้คอมไพเลอร์มั่นใจได้ว่าคุณทำทุกอย่างอย่างปลอดภัย เมื่อรันไทม์ ระบบจะลบประเภททั่วไปทั้งหมดออก ดังนั้นข้อความแสดงข้อผิดพลาดก่อนหน้านี้เกี่ยวกับการตรวจสอบประเภทที่ถูกลบจึงเกิดขึ้น

ปรากฏว่าคอมไพเลอร์สามารถสร้างโค้ดที่ถูกต้องได้โดยไม่ต้องเก็บประเภททั่วไปไว้จนกว่าจะถึงรันไทม์ แต่ก็หมายความว่าในบางครั้งคุณอาจทำบางอย่าง เช่น is การตรวจสอบประเภททั่วไป ซึ่งคอมไพเลอร์ไม่รองรับ Kotlin จึงเพิ่มประเภทที่ทำให้เป็นจริงหรือประเภทจริง

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

บทเรียนนี้มุ่งเน้นไปที่ Generics ซึ่งมีความสำคัญต่อการทำให้โค้ดมีความยืดหยุ่นมากขึ้นและนำกลับมาใช้ใหม่ได้ง่ายขึ้น

  • สร้างคลาสทั่วไปเพื่อให้โค้ดมีความยืดหยุ่นมากขึ้น
  • เพิ่มข้อจำกัดทั่วไปเพื่อจำกัดประเภทที่ใช้กับ Generics
  • ใช้ประเภท in และ out กับ Generics เพื่อให้การตรวจสอบประเภทดียิ่งขึ้นในการจำกัดประเภทที่ส่งผ่านเข้าหรือส่งคืนจากคลาส
  • สร้างฟังก์ชันและเมธอดทั่วไปเพื่อใช้กับประเภททั่วไป เช่น
    fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
  • ใช้ฟังก์ชันส่วนขยายทั่วไปเพื่อเพิ่มฟังก์ชันที่ไม่ใช่ฟังก์ชันหลักลงในคลาส
  • บางครั้งเราจำเป็นต้องใช้ประเภทที่ทำให้เป็นจริงได้เนื่องจากการลบประเภท ประเภทที่ทำให้เป็นจริงจะยังคงอยู่จนถึงรันไทม์ ซึ่งต่างจากประเภททั่วไป
  • ใช้ฟังก์ชัน check() เพื่อยืนยันว่าโค้ดทํางานตามที่คาดไว้ เช่น
    check(!waterSupply.needsProcessing) { "water supply needs processing first" }

เอกสารประกอบ Kotlin

หากต้องการข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อใดก็ตามในหลักสูตรนี้ หรือหากคุณติดขัด https://kotlinlang.org คือจุดเริ่มต้นที่ดีที่สุด

บทแนะนำ Kotlin

เว็บไซต์ https://try.kotlinlang.org มีบทแนะนำที่สมบูรณ์ซึ่งเรียกว่า Kotlin Koans, ตัวแปลภาษาบนเว็บ และชุดเอกสารอ้างอิงที่สมบูรณ์พร้อมตัวอย่าง

หลักสูตร Udacity

หากต้องการดูหลักสูตร Udacity ในหัวข้อนี้ โปรดดูค่ายฝึก Kotlin สำหรับโปรแกรมเมอร์

IntelliJ IDEA

เอกสารประกอบสำหรับ IntelliJ IDEA อยู่ในเว็บไซต์ของ JetBrains

ส่วนนี้แสดงรายการการบ้านที่เป็นไปได้สำหรับนักเรียน/นักศึกษาที่กำลังทำ Codelab นี้เป็นส่วนหนึ่งของหลักสูตรที่สอนโดยผู้สอน ผู้สอนมีหน้าที่ดำเนินการต่อไปนี้

  • มอบหมายการบ้านหากจำเป็น
  • สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานที่ได้รับมอบหมาย
  • ให้คะแนนงานการบ้าน

ผู้สอนสามารถใช้คำแนะนำเหล่านี้ได้มากน้อยตามที่ต้องการ และควรมีอิสระในการมอบหมายการบ้านอื่นๆ ที่เห็นว่าเหมาะสม

หากคุณกำลังทำ Codelab นี้ด้วยตนเอง โปรดใช้แบบฝึกหัดเหล่านี้เพื่อทดสอบความรู้ของคุณ

ตอบคำถามต่อไปนี้

คำถามที่ 1

ข้อใดต่อไปนี้คือรูปแบบการตั้งชื่อประเภททั่วไป

<Gen>

<Generic>

<T>

<X>

คำถามที่ 2

ข้อจำกัดเกี่ยวกับประเภทที่อนุญาตสำหรับประเภททั่วไปเรียกว่า

▢ การจํากัดทั่วไป

▢ ข้อจำกัดทั่วไป

▢ การแยกความกำกวม

▢ ขีดจำกัดประเภททั่วไป

คำถามที่ 3

Reified หมายถึง

▢ ระบบได้คำนวณผลกระทบจากการดำเนินการจริงของออบเจ็กต์แล้ว

▢ มีการตั้งค่าดัชนีรายการที่จำกัดในชั้นเรียน

▢ พารามิเตอร์ประเภททั่วไปได้รับการเปลี่ยนเป็นประเภทจริงแล้ว

▢ มีการทริกเกอร์ตัวบ่งชี้ข้อผิดพลาดระยะไกล

ไปยังบทเรียนถัดไป: 6. การดัดแปลงเชิงฟังก์ชัน

ดูภาพรวมของหลักสูตร รวมถึงลิงก์ไปยังโค้ดแล็บอื่นๆ ได้ที่ "Kotlin Bootcamp for Programmers: Welcome to the course"