Conditional Compilation ในแอปมือถือ — สาระสำคัญ ไดเรกทีฟ และหลักการทำงาน

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

Conditional Compilation ช่วยให้คอมไพเลอร์รวมหรือข้ามส่วนของซอร์สโค้ดขึ้นอยู่กับเงื่อนไขที่ทราบในขณะสร้าง ตาม The Swift Programming Language (2026) ไดเรกทีฟ #if จะถูกประมวลผลระหว่างการวิเคราะห์ AST ก่อนการสร้างโค้ดเครื่อง Conditional Compilation ช่วยให้นักพัฒนาสามารถดูแลฐานโค้ดเดียวสำหรับหลายแพลตฟอร์มและการกำหนดค่าโดยไม่ต้องทำซ้ำ

ประเด็นสำคัญ

  • Conditional Compilation — เทคนิคการคอมไพล์โค้ดแบบเลือกตามเงื่อนไขของแพลตฟอร์ม การกำหนดค่า หรือเวอร์ชันภาษา
  • ไดเรกทีฟ #if, #elseif, #else, #endif — โครงสร้างหลักของการคอมไพล์แบบมีเงื่อนไขใน Swift, C, C++, Objective-C
  • Kotlin ไม่มีไดเรกทีฟพรีโปรเซสเซอร์ — แต่จะใช้ BuildConfig, expect/actual และ sourceSets แทน
  • ข้อดี — โค้ดสำหรับแพลตฟอร์มที่ไม่เหมาะสมจะไม่ถูกคอมไพล์ ลดขนาดไบนารีและกำจัดข้อผิดพลาด
  • iOS/macOS โค้ดร่วม — Conditional Compilation เป็นพื้นฐานของการพัฒนาเฟรมเวิร์กข้ามแพลตฟอร์มของ Apple

Conditional Compilation คืออะไร

Conditional Compilation เป็นกลไกที่คอมไพเลอร์วิเคราะห์ไดเรกทีฟการคอมไพล์แบบมีเงื่อนไขและรวมเฉพาะบล็อกโค้ดที่ตรงตามเงื่อนไขในไบนารีเอาต์พุต ซึ่งช่วยให้มีฐานโค้ดเดียวที่ปรับให้เข้ากับแพลตฟอร์มเป้าหมายและการกำหนดค่าต่างๆ

แนวคิดนี้มาจาก C/C++ กับไดเรกทีฟพรีโปรเซสเซอร์ #ifdef, #ifndef, #endif ในภาษาสมัยใหม่ (Swift, Rust, Go) กลไกทำงานที่ระดับคอมไพเลอร์โดยไม่มีพรีโปรเซสเซอร์แยกต่างหาก ซึ่งเพิ่มความปลอดภัย: บล็อกแบบมีเงื่อนไขต้องถูกต้องตามไวยากรณ์แม้ว่าจะไม่ได้ถูกคอมไพล์

ตามเซสชัน Apple WWDC “Embrace Swift” (2025) ประมาณ 40% ของโปรเจกต์ Swift ใช้การคอมไพล์แบบมีเงื่อนไขเพื่อรองรับ iOS และ macOS ในเป้าหมายเดียว สำหรับโปรเจกต์ที่ใช้ UIKit และ SwiftUI โค้ด UI มักถูกแบ่งด้วยไดเรกทีฟ #if os(iOS) และ #if os(macOS) ทำให้สามารถใช้ตรรกะทางธุรกิจซ้ำได้

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

Conditional Compilation ใน Swift

Swift มีไดเรกทีฟหลักสี่ประการ: #if, #elseif, #else, #endif ต่างจากพรีโปรเซสเซอร์ C ตรงที่ Swift ต้องการความถูกต้องตามไวยากรณ์ของโค้ดในทุกสาขา — คอมไพเลอร์จะแยกวิเคราะห์โค้ดทั้งหมดแต่สร้างโค้ดเครื่องเฉพาะสาขาที่ทำงานอยู่

การตรวจสอบแพลตฟอร์ม os()

Swift รองรับฟังก์ชันตรวจสอบในตัว: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows) ฟังก์ชันเหล่านี้ตรวจสอบแพลตฟอร์มเป้าหมายที่กำลังสร้างแอปพลิเคชัน การรวมกับ && และ || ช่วยให้สร้างเงื่อนไขที่ซับซ้อนได้

swift
// โค้ดเดียวสําหรับ iOS, macOS และ tvOS
import Foundation

class PlatformService {
    func getSystemVersion() -> String {
        #if os(iOS) || os(tvOS)
            return UIDevice.current.systemVersion
        #elseif os(macOS)
            let vers = ProcessInfo.processInfo.operatingSystemVersion
            return "\(vers.majorVersion).\(vers.minorVersion)"
        #else
            return "unknown"
        #endif
    }
}

การตรวจสอบเวอร์ชันคอมไพเลอร์

Swift รองรับการตรวจสอบเวอร์ชันคอมไพเลอร์: #if swift(>=5.9) ซึ่งมีประโยชน์สำหรับไลบรารีและเฟรมเวิร์กที่รองรับ Swift หลายเวอร์ชัน คุณสมบัติภาษาใหม่ (เช่น มาโครใน Swift 5.9) สามารถป้องกันได้ด้วยการตรวจสอบดังกล่าว

swift
// ความเข้ากันได้ย้อนหลัง
#if swift(>=5.9)
    @MainActor
    struct ModernView: View {
        var body: some View {
            Text("Modern SwiftUI")
        }
    }
#else
    struct ModernView: View {
        var body: some View {
            Text("Legacy SwiftUI")
        }
    }
#endif

การตรวจสอบความพร้อมใช้งานของโมดูล canImport()

ฟังก์ชัน canImport(ModuleName) จะตรวจสอบว่าโมดูลที่ระบุพร้อมใช้งานในสภาพแวดล้อมการสร้างปัจจุบันหรือไม่ นี่เป็นกลไกที่ยืดหยุ่นที่สุด: ไม่ผูกติดกับแพลตฟอร์มเฉพาะ ตัวอย่างเช่น โค้ดที่ใช้ CoreHaptics จะคอมไพล์เฉพาะบนอุปกรณ์ที่มีเฟรมเวิร์กนี้พร้อมใช้งาน

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // การใช้งานการตอบสนองแบบสัมผัส
        }
    }
#endif

ทางเลือกใน Kotlin และ Android

Kotlin ในฐานะภาษาไม่มีไดเรกทีฟพรีโปรเซสเซอร์ แต่ระบบนิเวศ Android มีทางเลือกสามทาง: ฟิลด์ BuildConfig (การตรวจสอบในรันไทม์), sourceSets (การแทนที่ทั้งไฟล์) และ expect/actual (ใน Kotlin Multiplatform)

Source Sets ใน Gradle

sourceSets ของ Gradle ช่วยให้มีการimplement คลาสที่แตกต่างกันสำหรับ flavor หรือประเภทบิวด์ที่แตกต่างกัน ในไดเรกทอรี src/debug/ คือ implement สำหรับดีบัก ใน src/release/ — สำหรับรีลีส ขณะสร้าง Gradle จะเลือก sourceSet ที่เหมาะสมและคอมไพล์เฉพาะไฟล์ของมัน

kotlin
// src/debug/kotlin/com/example/Logger.kt
object Logger {
    fun log(tag: String, message: String) {
        Log.d(tag, message)
    }
}

// src/release/kotlin/com/example/Logger.kt
object Logger {
    fun log(tag: String, message: String) {
        // No-op ในรีลีส
    }
}

Expect/Actual ใน Kotlin Multiplatform

KMP มีกลไก expect (การประกาศในโค้ดร่วม) และ actual (implement สำหรับแพลตฟอร์มเฉพาะ) นี่เป็นกลไกในเวลาคอมไพล์: สำหรับ iOS จะคอมไพล์ implement actual จาก iOS sourceSet สำหรับ Android — จาก Android sourceSet implement ที่ไม่ใช่เป้าหมายจะไม่ถูกคอมไพล์

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

// androidMain — actual สําหรับ Android
actual fun getPlatformName(): String =
    "Android \${Build.VERSION.SDK_INT}"

// iosMain — actual สําหรับ iOS
actual fun getPlatformName(): String =
    UIDevice.current.systemName() + " " + UIDevice.current.systemVersion

พรีโปรเซสเซอร์ C/C++ และ NDK

เมื่อพัฒนาไลบรารีเนทีฟผ่าน Android NDK จะใช้พรีโปรเซสเซอร์ C/C++ แบบคลาสสิกกับไดเรกทีฟ #ifdef, #ifndef, #define ต่างจาก Swift ตรงที่พรีโปรเซสเซอร์ C ทำงานที่ระดับข้อความ — โค้ดในสาขาที่ไม่ได้ทำงานอาจไม่ถูกต้องตามไวยากรณ์

แฟลกแพลตฟอร์ม NDK

NDK กำหนดแมโครสำหรับแต่ละแพลตฟอร์ม: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux) สำหรับสถาปัตยกรรม: __arm__, __aarch64__, __x86_64__ แมโครเหล่านี้ถูกตั้งค่าโดยอัตโนมัติโดยคอมไพเลอร์เมื่อสร้างสำหรับแพลตฟอร์มเป้าหมาย

cpp
// โค้ดเนทีฟสําหรับ Android และ iOS
#include <cstdint>

#ifdef __ANDROID__
    int32_t getJniEnv(JNIEnv* env) {
        return env->GetVersion();
    }
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
    int32_t getOsVersion() {
        #if TARGET_OS_IOS
            return "iOS";
        #elif TARGET_OS_OSX
            return "macOS";
        #endif
    }
#endif

เมื่อทำงานกับ NDK สิ่งสำคัญคือต้องจำว่าพรีโปรเซสเซอร์ C/C++ เป็น การแทนที่ข้อความ หากมีข้อผิดพลาดทางไวยากรณ์ในสาขาที่ไม่ได้ทำงาน คอมไพเลอร์จะไม่เห็นมัน แต่ถ้า #define ที่ผิดพลาดทำลายสาขาที่ทำงาน — ข้อผิดพลาดจะปรากฏขึ้น แนะนำให้ลดห่วงโซ่ #define และใช้ค่าคงที่ constexpr

สำหรับ Rust ซึ่งใช้ในการพัฒนา mobile ผ่าน UniFFI และ Mozilla Application Services เช่นกัน มีกลไกของตัวเอง — แฟลกคุณสมบัติใน Cargo.toml แฟลกเช่น #[cfg(target_os = “android”)] ใน Rust ทำงานคล้ายกับไดเรกทีฟ Swift: การตรวจสอบทำที่ระดับคอมไพเลอร์ ไม่ใช่พรีโปรเซสเซอร์ สิ่งนี้ทำให้ Rust เป็นตัวเลือกที่น่าสนใจสำหรับไลบรารีเนทีฟที่ต้องคอมไพล์สำหรับ Android และ iOS จากฐานโค้ดเดียว

สถานการณ์เชิงปฏิบัติและแอนติแพตเทิร์น

Conditional Compilation มีประสิทธิภาพในสถานการณ์ที่กำหนดอย่างเคร่งครัด เมื่อใช้ไม่ถูกต้อง จะสร้าง กลิ่นโค้ด (code smell) ที่ทดสอบและบำรุงรักษาได้ยาก มาดูสถานการณ์ที่ถูกต้องและข้อผิดพลาดทั่วไป

สถานการณ์ที่ถูกต้อง

สถานการณ์แรก — นามธรรมของแพลตฟอร์ม: façade เดียวที่ภายใน Conditional Compilation เลือก implement ของแพลตฟอร์ม สถานการณ์ที่สอง — การดีบักและการทำโปรไฟล์: เครื่องมือของนักพัฒนาที่ไม่ควรไปถึงรีลีส สถานการณ์ที่สาม — ความเข้ากันได้ย้อนหลัง: การรองรับเวอร์ชัน OS เก่าจนกว่าเวอร์ชันต่ำสุดจะถูกอัปเดต

สถานการณ์ภาษาเงื่อนไข
นามธรรมของแพลตฟอร์มSwift#if os(iOS)
การดีบักSwift/ObjC#if DEBUG
ความเข้ากันได้ย้อนหลังSwift#if swift(>=5.7)
ไลบรารีเนทีฟC/C++#ifdef __ANDROID__
การทดสอบ A/BJava/KotlinBuildConfig.FLAVOR

แอนติแพตเทิร์น

แอนติแพตเทิร์นที่อันตรายที่สุด — การแพร่กระจายของไดเรกทีฟทั่วโค้ด หากทุกไฟล์ที่สองมี #if นั่นเป็นสัญญาณว่าสถาปัตยกรรมต้องการการปรับโครงสร้าง วิธีแก้ไขที่ถูกต้องคือดึงโค้ดแพลตฟอร์มไปไว้หลังโปรโตคอล/อินเทอร์เฟซและใช้ Dependency Injection

  • #if ในทุกไฟล์ — แอนติแพตเทิร์นทางสถาปัตยกรรม โค้ดแพลตฟอร์มควรถูกแยกไว้หลังโปรโตคอล
  • #if ซ้อนกัน — กลายเป็นอ่านยากอย่างรวดเร็ว ความลึกของการซ้อนไม่ควรเกิน 2 ระดับ
  • การทำซ้ำทั้งฟังก์ชัน — หากฟังก์ชันถูกคัดลอกทั้งหมดใน #if และ #else ควรดึงไปไว้ในส่วนร่วม
  • การทดสอบ — โค้ดภายในสาขาที่ไม่ได้ทำงานไม่ได้รับการทดสอบ จำเป็นต้องมี CI build ของทุกชุดค่าผสมที่เป็นไปได้
  • แฟลกเวทมนตร์ — แฟลกที่ไม่มีเอกสารซึ่งทีมพัฒนาชุดใหม่ไม่รู้จัก

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

Conditional Compilation แตกต่างจากการตรวจสอบในรันไทม์อย่างไร

Conditional Compilation ทำงาน ในเวลาคอมไพล์: โค้ดที่ไม่ได้ใช้งานจะไม่เข้าไปในไบนารี การตรวจสอบในรันไทม์ (if / switch) จะถูกคอมไพล์เสมอ เงื่อนไขจะถูกตรวจสอบระหว่างการทำงาน วิธีแรกปลอดภัยกว่าและมีประสิทธิภาพกว่า วิธีที่สองยืดหยุ่นกว่า (สามารถเปลี่ยนได้โดยไม่ต้องสร้างใหม่)

สามารถใช้ #if ภายในฟังก์ชันใน Swift ได้หรือไม่

ได้ Swift อนุญาตให้ใช้ไดเรกทีฟ #if ภายในฟังก์ชัน ลูป และแม้แต่ภายในนิพจน์ นี่เป็นหนึ่งในคุณสมบัติที่ขาดหายไปใน Swift เวอร์ชันแรกๆ ตัวอย่าง: let x = #if DEBUG 1 #else 0 #endif — โค้ดที่ถูกต้อง

ทำไม Kotlin ไม่เพิ่มพรีโปรเซสเซอร์

นักพัฒนา Kotlin จงใจปฏิเสธพรีโปรเซสเซอร์ โดยถือว่ามันเป็นแหล่งของ โค้ดที่เปราะบาง แต่พวกเขาเสนอ expect/actual (ความปลอดภัยในเวลาคอมไพล์) และ sourceSets ของ Gradle (การแยกที่ระดับไฟล์) ทั้งสองวิธีมีความน่าเชื่อถือมากกว่าการแทนที่ข้อความ

วิธีทดสอบโค้ดภายในสาขา #if ที่ไม่ได้ทำงาน

สร้างแอปพลิเคชันด้วยชุดค่าผสมของแฟลกต่างๆ ใน CI สำหรับ Swift: กำหนดค่า Xcode schemes แยกต่างหากด้วย Active Compilation Conditions ที่แตกต่างกัน สำหรับ Android: กำหนดค่า Build Variants แยกต่างหากและรันการทดสอบสำหรับแต่ละรายการ ระบบอัตโนมัติเป็นสิ่งจำเป็น

จะเกิดอะไรขึ้นถ้าเงื่อนไข #if มีข้อผิดพลาดทางไวยากรณ์

ใน Swift เงื่อนไข #if เป็นไดเรกทีฟของคอมไพเลอร์ หากเงื่อนไข本身ไม่ถูกต้องตามไวยากรณ์ (เช่น พิมพ์ชื่อ os() ผิด) คอมไพเลอร์จะแสดง ข้อผิดพลาดในการคอมไพล์ ใน C/C++ พรีโปรเซสเซอร์จะไม่พบแมโครและเงื่อนไขจะกลายเป็นเท็จ

สรุป

  • Conditional Compilation — เทคนิคการคอมไพล์ที่แยกโค้ดที่ไม่ใช่เป้าหมายในเวลาสร้าง แตกต่างจากการตรวจสอบในรันไทม์
  • Swift รองรับ #if กับ os(), canImport(), swift() — ไดเรกทีฟที่ปลอดภัยในเวลาคอมไพล์ซึ่งต้องการความถูกต้องตามไวยากรณ์ของทุกสาขา
  • Kotlin ใช้ expect/actual และ sourceSets ของ Gradle แทนพรีโปรเซสเซอร์ — วิธีที่เชื่อถือได้มากกว่าแต่ยืดหยุ่นน้อยกว่า
  • C/C++ ใน NDK ใช้พรีโปรเซสเซอร์ข้อความแบบคลาสสิก #ifdef / #ifndef กับแมโครแพลตฟอร์ม __ANDROID__, __APPLE__
  • การใช้ที่ถูกต้อง — นามธรรมของแพลตฟอร์ม การดีบัก ความเข้ากันได้ย้อนหลัง การใช้ไม่ถูกต้อง — #if ในทุกไฟล์ การซ้อนลึก แฟลกเวทมนตร์
  • CI เป็นสิ่งจำเป็น — ทุกชุดค่าผสมของแฟลกต้องถูกสร้างและทดสอบโดยอัตโนมัติ มิฉะนั้นโค้ดในสาขาที่ไม่ได้ทำงานจะกลายเป็นโค้ดที่ตายแล้ว

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

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

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

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