expect/actual — แนวคิด คีย์เวิร์ด KMM และการทำงาน

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-06-05 เวลาอ่าน: 8 นาที

expect/actual — กลไกของ Kotlin Multiplatform ที่ช่วยให้ประกาศ API ที่ขึ้นกับแพลตฟอร์มในโค้ดทั่วไปได้ คีย์เวิร์ด expect สร้างสัญญาของฟังก์ชัน คลาส หรือคุณสมบัติใน commonMain ส่วนคีย์เวิร์ด actual จัดเตรียม implementation ที่เป็นรูปธรรมสำหรับแต่ละแพลตฟอร์ม คอมไพเลอร์จะตรวจสอบว่า expect-declaration ทุกตัวมี actual-implementation ที่สอดคล้องกันในทุกแพลตฟอร์มเป้าหมาย ตามข้อมูลของ JetBrains, 2025 กลไกนี้ถูกใช้ใน 80% ของโปรเจกต์ KMM เพื่อ implement โลจิกทางธุรกิจเฉพาะแพลตฟอร์ม

สิ่งสำคัญ

  • expect — คีย์เวิร์ดสำหรับประกาศสัญญาของฟังก์ชัน คลาส หรือคุณสมบัติในโค้ดทั่วไป
  • actual — คีย์เวิร์ดสำหรับจัดเตรียม implementation ของ expect-declaration สำหรับแพลตฟอร์ม
  • commonMain — source set ของโค้ดทั่วไปซึ่งใช้เก็บ expect-declaration
  • การตรวจสอบของคอมไพเลอร์ — คอมไพเลอร์รับประกันว่า actual-implementation มีอยู่สำหรับทุกแพลตฟอร์มเป้าหมาย
  • Source set — ชุด (iosMain, androidMain) ซึ่งเก็บ actual-implementation สำหรับแพลตฟอร์มต่างๆ

expect/actual คืออะไร?

expect/actual — คือกลไกแบบประกาศของ Kotlin Multiplatform สำหรับการเขียนโปรแกรมเชิงแพลตฟอร์ม โดยอนุญาตให้อธิบาย API หนึ่งครั้งในโมดูลทั่วไป (expect) และ implement แยกกันสำหรับแต่ละแพลตฟอร์ม (actual) ต่างจากอินเทอร์เฟซ expect/actual ไม่สร้าง virtual call — คอมไพเลอร์เชื่อมโยง expect- และ actual-declaration ในขั้นตอนการคอมไพล์ ซึ่งช่วยลด overhead จากการ dispatch แบบไดนามิก

ประวัติของ expect/actual เริ่มต้นจากการปรากฏของ Kotlin Multiplatform ในปี 2017 เริ่มแรกกลไกนี้มีชื่อว่า expect/actual declarations และเป็นแบบทดลอง ใน Kotlin 1.2 มีการเพิ่ม expect-annotation และใน Kotlin 1.3 expect/actual ก็กลายเป็นเสถียรสำหรับคลาสและฟังก์ชัน กลไกได้ถูกขยายเพิ่มขึ้นเรื่อยๆ: ใน Kotlin 1.6 เพิ่มการรองรับ expect/actual สำหรับ companion-object ใน Kotlin 1.7 สำหรับ enum-class และใน Kotlin 2.0 สำหรับ typealias

คุณสมบัติสำคัญของ expect/actual คือ ความปลอดภัยในระดับการคอมไพล์ หากนักพัฒนาเพิ่ม expect-declaration ใน commonMain แต่ลืมจัดเตรียม actual-implementation สำหรับ iOS คอมไพเลอร์จะแจ้งข้อผิดพลาด ซึ่งป้องกัน runtime-failure ที่เกิดขึ้นในแนวทางที่ใช้ reflection หรือการโหลดโค้ดเฉพาะแพลตฟอร์มแบบไดนามิก

กลไก expect/actual ทำงานอย่างไร

กลไก expect/actual ทำงานในระดับ source set — ระบบโมดูลของ Kotlin Multiplatform โค้ดทั่วไปที่สามารถเข้าถึงได้ทุกแพลตฟอร์มจะอยู่ใน source set commonMain โค้ดที่ขึ้นกับแพลตฟอร์มอยู่ใน iosMain, androidMain, macosMain และอื่นๆ คีย์เวิร์ด expect ใน commonMain ประกาศ API ส่วนคีย์เวิร์ด actual ใน source set เฉพาะแพลตฟอร์มจะจัดเตรียม implementation คอมไพเลอร์เชื่อมโยงพวกมันในขั้นตอน code generation โดยแทนที่การเรียกใช้ expect-function ด้วย actual-implementation สำหรับแพลตฟอร์มเป้าหมาย

ลำดับชั้นของ source set ในโปรเจกต์ KMM ทั่วไปมีลักษณะดังนี้: commonMain ประกอบด้วย expect-declaration, iosMain และ androidMain ประกอบด้วย actual-implementation เมื่อคอมไพล์สำหรับ iOS จะใช้ actual จาก iosMain เมื่อคอมไพล์สำหรับ Android จะใช้จาก androidMain Source set อาจมีลำดับชั้นกลาง (เช่น iosArm64Main สำหรับสถาปัตยกรรมเฉพาะ) ซึ่งช่วยให้ปรับแต่ง implementation สำหรับอุปกรณ์ต่างๆ ได้

kotlin
// commonMain — การประกาศ expect
expect fun getPlatformName(): String

// androidMain — actual สำหรับ Android
actual fun getPlatformName(): String = "Android"

// iosMain — actual สำหรับ iOS
actual fun getPlatformName(): String = "iOS"

การตรวจสอบ actual-implementation ของคอมไพเลอร์

Kotlin-compiler ตรวจสอบเงื่อนไขหลายประการเมื่อทำงานกับ expect/actual แต่ละ expect-declaration ต้องมี actual-implementation สำหรับทุกแพลตฟอร์มที่ใช้งานอยู่ ลายเซ็นของ actual-declaration ต้องตรงกับ expect-signature (annotation @OptionalExpectation อาจผ่อนปรนข้อกำหนดนี้) ตัวปรับการเข้าถึง return type และพารามิเตอร์ต้องเหมือนกัน คอมไพเลอร์ยังตรวจสอบว่าไม่มี cyclic dependencies ระหว่าง expect- และ actual-declaration

ประเภทของ expect/actual: ฟังก์ชัน คลาส คุณสมบัติ

expect/actual รองรับการประกาศหลายประเภท ที่พบบ่อยที่สุดคือ expect/actual-function สำหรับการดำเนินการเฉพาะแพลตฟอร์ม expect/actual-class สำหรับออบเจ็กต์ที่ต้องการ native implementation และ expect/actual-property สำหรับค่าคงที่และการตั้งค่า แต่ละประเภทมีกฎการใช้งานและข้อจำกัดของตนเอง

Expect/actual-function — เป็นประเภทที่ง่ายและพบบ่อยที่สุด ใช้สำหรับเรียก API ของแพลตฟอร์ม เช่น การรับเวลา การอ่านไฟล์ หรือการส่ง HTTP-request Expect/actual-class ใช้สำหรับสร้างออบเจ็กต์ที่โต้ตอบโดยตรงกับ native code (เช่น การเข้าถึงกล้อง ตำแหน่งที่ตั้ง หรือ key storage) Expect/actual-property (val) เหมาะสำหรับค่าคงที่ของแพลตฟอร์ม — ชื่อ OS เวอร์ชัน SDK หรือพาธไปยังไดเรกทอรีของระบบ

ประเภทการประกาศคีย์เวิร์ดตัวอย่างการใช้งาน
ฟังก์ชันexpect fun / actual funรับค่า unique identifier ของอุปกรณ์
คลาสexpect class / actual classเข้าถึง SecureStorage (Keychain / EncryptedSharedPreferences)
คุณสมบัติexpect val / actual valแพลตฟอร์มปัจจุบัน (iOS / Android)
Enum-คลาสexpect enum / actual enumรายการ permission ที่มีในแอปพลิเคชัน
Typealiasexpect typealias / actual typealiasประเภทของ network response เฉพาะแพลตฟอร์ม

ข้อจำกัดของ expect/actual

ไม่ใช่ทุกโครงสร้างของ Kotlin ที่สามารถใช้กับ expect/actual ได้ Expect-declaration ไม่สามารถมีเนื้อหา — มีเพียงลายเซ็นเท่านั้น Expect-class ไม่สามารถมี constructor พร้อมพารามิเตอร์ (ต้องเป็น primary constructor ว่าง) สำหรับ enum expect/actual ค่าคงที่ทั้งหมดต้องเหมือนกันใน expect และ actual Expect-property ต้องเป็น val (ไม่ใช่ var) เนื่องจากการเก็บสถานะในโมดูลทั่วไปสำหรับคุณสมบัติเฉพาะแพลตฟอร์มนั้นไม่มีความหมาย

ตัวอย่างโค้ด: จากง่ายไปซับซ้อน

มาดูตัวอย่างเชิงปฏิบัติของ expect/actual ตั้งแต่ฟังก์ชันง่ายๆ ไปจนถึงคลาสที่สมบูรณ์ กรณีพื้นฐาน — การรับชื่อของแพลตฟอร์มเพื่อใช้ใน UI ตัวอย่างที่ซับซ้อนขึ้นรวมถึงการเข้าถึง native storage และการทำงานกับ platform threads

kotlin
// commonMain — คลาส expect สำหรับพื้นที่เก็บข้อมูลที่ปลอดภัย
expect class PlatformStorage {
    fun save(key: String, value: String)
    fun get(key: String): String?
    fun remove(key: String)
}

// androidMain — actual บน Android
actual class PlatformStorage {
    private val prefs = AppContext.getSharedPreferences("secure", 0)

    actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
    actual fun get(key: String): String? = prefs.getString(key, null)
    actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}

ในตัวอย่างนี้ expect-class PlatformStorage กำหนดสัญญาของ storage แบบ key-value อย่างง่าย บน Android implementation ใช้ SharedPreferences ส่วนบน iOS ใช้ Keychain หรือ NSUserDefaults ด้วย expect/actual business logic ใน commonMain สามารถเรียก save/get/remove โดยไม่จำเป็นต้องรู้เกี่ยวกับ implementation ของแพลตฟอร์ม

kotlin
// iosMain — actual บน iOS ด้วย Keychain
actual class PlatformStorage {
    actual fun save(key: String, value: String) {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key,
            kSecValueData to value.encodeToByteArray()
        )
        SecItemAdd(query, null)
    }

    actual fun get(key: String): String? {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key,
            kSecReturnData to true
        )
        val result = mutableMapOf<String, Any>()
        return if (SecItemCopyMatching(query, result) == errSecSuccess)
            result[kSecValueData]?.toString()
        else null
    }

    actual fun remove(key: String) {
        val query = mapOf<String, Any>(
            kSecClass to kSecClassGenericPassword,
            kSecAttrAccount to key
        )
        SecItemDelete(query)
    }
}

แนวทางปฏิบัติที่ดีที่สุดสำหรับ expect/actual

ในการออกแบบ API ของ expect/actual ควรยึดหลักการหลายประการ ลดจำนวน expect-declaration ให้มากที่สุด — ยิ่งมีโค้ดทั่วไปมากเท่าไหร่ การบำรุงรักษาก็ยิ่งง่ายขึ้น ใช้ expect/actual เฉพาะกับ API ที่แตกต่างกันในแต่ละแพลตฟอร์มเท่านั้น สำหรับโค้ดที่เหลือ ให้ใช้อินเทอร์เฟซกับ factory หรือ dependency injection ซึ่งช่วยให้การทดสอบง่ายขึ้น

แนะนำให้จัดกลุ่ม expect-declaration ตาม โมดูลตามหัวข้อ แทนที่จะปะปนกันในไฟล์เดียว เช่น Storage.kt สำหรับ expect-declaration ของ storage, Platform.kt สำหรับ expect-function ที่ทำงานกับ OS และ Analytics.kt สำหรับ expect-class ด้าน analytics ช่วยให้การนำทางและทำความเข้าใจพื้นผิวของแพลตฟอร์มในโปรเจกต์ KMM ง่ายขึ้น แต่ละ actual-file ควรอยู่ใน source set ที่เกี่ยวข้อง: androidMain, iosMain, desktopMain และอื่นๆ

Default-implementation ผ่าน expect fun กับ actual fun โดยที่ actual ใช้ common-code — เป็น anti-pattern ที่พบบ่อย หาก implementation ของแพลตฟอร์มไม่แตกต่างจาก default ก็ไม่จำเป็นต้องใช้ expect/actual ในกรณีเช่นนี้ ให้ใช้ฟังก์ชันธรรมดาใน commonMain นอกจากนี้ หลีกเลี่ยง expect/actual สำหรับ getter ธรรมดา — ให้ใช้ expect val กับค่าคงที่

การจัดระเบียบโค้ดในโปรเจกต์

โครงสร้างที่ถูกต้องของโค้ด expect/actual มีความสำคัญต่อการอ่านโปรเจกต์ แต่ละโมดูล expect/actual ควรมีจุดเริ่มต้นเดียว ตัวอย่างการจัดระเบียบ: commonMain/kotlin/com/project/platform ประกอบด้วย expect-declaration, androidMain/kotlin/com/project/platform — actual สำหรับ Android, iosMain/kotlin/com/project/platform — actual สำหรับ iOS ชื่อไฟล์และแพ็กเกจควรตรงกันสำหรับ expect และ actual เพื่อให้นักพัฒนาสามารถค้นหา implementation ที่สอดคล้องกันได้อย่างรวดเร็ว

ทางเลือกอื่นของ expect/actual ใน KMM

อินเทอร์เฟซ กับ platform factory — เป็นทางเลือกหลักของ expect/actual แทนที่จะใช้ expect-class สามารถประกาศอินเทอร์เฟซใน commonMain และสร้าง concrete class ในโมดูลเฉพาะแพลตฟอร์ม Factory หรือ dependency injection container จัดเตรียม implementation ที่ถูกต้องใน runtime แนวทางนี้เหมาะสำหรับการทดสอบมากกว่า เนื่องจากอินเทอร์เฟซสามารถ mock ได้

Dependency Injection (Koin, Kodein) — เป็นแนวทางที่ยืดหยุ่นกว่าแต่มีประสิทธิภาพน้อยกว่า DI-container ถูกกำหนดแยกกันสำหรับแต่ละแพลตฟอร์มและจัดเตรียม dependencies เฉพาะแพลตฟอร์มให้กับ common-code ต่างจาก expect/actual การ injection เกิดขึ้นใน runtime ซึ่งทำให้สามารถสลับ implementation สำหรับการทดสอบได้ ในทางกลับกัน ข้อผิดพลาดในการกำหนดค่า DI จะถูกตรวจพบเมื่อรันเท่านั้น ไม่ใช่ในขั้นตอนการคอมไพล์

แนวทางการตรวจสอบในขั้นตอนการคอมไพล์ความยืดหยุ่นในการทดสอบค่าใช้จ่ายใน Runtime
expect/actualสมบูรณ์ต่ำ (actual ไม่สามารถ mock ได้)เป็นศูนย์ (การเชื่อมโยงตอนคอมไพล์)
อินเทอร์เฟซ + factoryบางส่วนสูง (สามารถ mock ได้)น้อยที่สุด (virtual call)
Dependency Injectionไม่มี (runtime)สูงปานกลาง (DI-proxy)

การเลือกระหว่าง expect/actual กับทางเลือกอื่นขึ้นอยู่กับบริบท สำหรับ ประสิทธิภาพที่สำคัญ (game engine, real-time processing) expect/actual เหมาะสมกว่าเนื่องจากไม่มีค่าใช้จ่ายเพิ่มเติม สำหรับ business logic (repository, use-case) ควรใช้อินเทอร์เฟซกับ DI เพื่อให้การทดสอบง่ายขึ้น แนวทางแบบผสม — expect/actual สำหรับการดำเนินการเฉพาะแพลตฟอร์มระดับต่ำและอินเทอร์เฟซสำหรับชั้น business logic — ถูกใช้ในโปรเจกต์ KMM ระดับ production ส่วนใหญ่

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

expect/actual ต่างจากอินเทอร์เฟซอย่างไร?

expect/actual เชื่อมโยง implementation ในขั้นตอนการคอมไพล์โดยไม่มี virtual call ส่วนอินเทอร์เฟซทำใน runtime expect/actual รับประกันว่า implementation มีอยู่สำหรับทุกแพลตฟอร์ม ส่วนอินเทอร์เฟซต้องการการตรวจสอบใน runtime

สามารถใช้ expect/actual สำหรับ enum ได้หรือไม่?

ได้ expect enum รองรับตั้งแต่ Kotlin 1.7 เป็นต้นไป ค่าคงที่ทั้งหมดใน expect และ actual enum ต้องตรงกัน ค่าคงที่ที่แตกต่างกันในแต่ละแพลตฟอร์มเป็นข้อผิดพลาดในการคอมไพล์

จะเกิดอะไรขึ้นหากลืม actual-implementation?

คอมไพเลอร์ จะแจ้งข้อผิดพลาดสำหรับแต่ละแพลตฟอร์มที่ไม่มี actual-implementation โปรเจกต์จะไม่สามารถ build ได้จนกว่า expect-declaration ทุกตัวจะมี actual-implementation ที่สอดคล้องกัน

สามารถใช้ expect/actual ภายใน source set เดียวกันได้หรือไม่?

ไม่ได้ expect และ actual ต้องอยู่ใน source set ที่ต่างกัน expect — ใน commonMain หรือ source set ระดับกลาง actual — ใน source set เฉพาะแพลตฟอร์ม การวาง expect และ actual ใน source set เดียวกันเป็นข้อผิดพลาดในการคอมไพล์

จะทดสอบโค้ด expect/actual ได้อย่างไร?

สำหรับการทดสอบ expect/actual ให้ใช้ commonTest กับ source set สำหรับทดสอบเฉพาะแพลตฟอร์ม เขียน expect-test ใน commonTest และ actual-test สำหรับแต่ละแพลตฟอร์ม Integration test จะรันแยกกันในแต่ละแพลตฟอร์มเป้าหมาย

สรุป

  • expect/actual — กลไกสำคัญของ Kotlin Multiplatform สำหรับ implementation เฉพาะแพลตฟอร์มพร้อมการตรวจสอบโดยคอมไพเลอร์
  • expect ประกาศสัญญาใน commonMain, actual จัดเตรียม implementation ใน source set เฉพาะแพลตฟอร์ม
  • ประเภทการประกาศรวมถึงฟังก์ชัน คลาส คุณสมบัติ enum-class และ typealias พร้อมกฎการใช้งานที่แตกต่างกัน
  • การตรวจสอบโดยคอมไพเลอร์ รับประกันว่า actual-implementation มีอยู่สำหรับทุกแพลตฟอร์มเป้าหมาย ป้องกัน runtime-failure
  • แนะนำ ให้ลดจำนวน expect/actual และใช้อินเทอร์เฟซกับ DI สำหรับ business logic
  • การจัดระเบียบโค้ด ควรเป็นแบบเดียวกันกับชื่อไฟล์และแพ็กเกจที่ตรงกันสำหรับ expect และ actual
  • ใช้ expect/actual สำหรับการดำเนินการเฉพาะแพลตฟอร์มระดับต่ำ (storage, file system, sensors) — ซึ่งช่วยให้ไม่มีค่าใช้จ่ายเพิ่มเติมใน runtime

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม