Conditional Compilation ช่วยให้คอมไพเลอร์รวมหรือข้ามส่วนของซอร์สโค้ดขึ้นอยู่กับเงื่อนไขที่ทราบในขณะสร้าง ตาม The Swift Programming Language (2026) ไดเรกทีฟ #if จะถูกประมวลผลระหว่างการวิเคราะห์ AST ก่อนการสร้างโค้ดเครื่อง 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 และในทางกลับกัน การตรวจสอบในรันไทม์ไม่ให้การรับประกันดังกล่าว
Swift มีไดเรกทีฟหลักสี่ประการ: #if, #elseif, #else, #endif ต่างจากพรีโปรเซสเซอร์ C ตรงที่ Swift ต้องการความถูกต้องตามไวยากรณ์ของโค้ดในทุกสาขา — คอมไพเลอร์จะแยกวิเคราะห์โค้ดทั้งหมดแต่สร้างโค้ดเครื่องเฉพาะสาขาที่ทำงานอยู่
Swift รองรับฟังก์ชันตรวจสอบในตัว: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows) ฟังก์ชันเหล่านี้ตรวจสอบแพลตฟอร์มเป้าหมายที่กำลังสร้างแอปพลิเคชัน การรวมกับ && และ || ช่วยให้สร้างเงื่อนไขที่ซับซ้อนได้
// โค้ดเดียวสําหรับ 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) สามารถป้องกันได้ด้วยการตรวจสอบดังกล่าว
// ความเข้ากันได้ย้อนหลัง
#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(ModuleName) จะตรวจสอบว่าโมดูลที่ระบุพร้อมใช้งานในสภาพแวดล้อมการสร้างปัจจุบันหรือไม่ นี่เป็นกลไกที่ยืดหยุ่นที่สุด: ไม่ผูกติดกับแพลตฟอร์มเฉพาะ ตัวอย่างเช่น โค้ดที่ใช้ CoreHaptics จะคอมไพล์เฉพาะบนอุปกรณ์ที่มีเฟรมเวิร์กนี้พร้อมใช้งาน
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// การใช้งานการตอบสนองแบบสัมผัส
}
}
#endif
Kotlin ในฐานะภาษาไม่มีไดเรกทีฟพรีโปรเซสเซอร์ แต่ระบบนิเวศ Android มีทางเลือกสามทาง: ฟิลด์ BuildConfig (การตรวจสอบในรันไทม์), sourceSets (การแทนที่ทั้งไฟล์) และ expect/actual (ใน Kotlin Multiplatform)
sourceSets ของ Gradle ช่วยให้มีการimplement คลาสที่แตกต่างกันสำหรับ flavor หรือประเภทบิวด์ที่แตกต่างกัน ในไดเรกทอรี src/debug/ คือ implement สำหรับดีบัก ใน src/release/ — สำหรับรีลีส ขณะสร้าง Gradle จะเลือก sourceSet ที่เหมาะสมและคอมไพล์เฉพาะไฟล์ของมัน
// 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 ในรีลีส
}
}
KMP มีกลไก expect (การประกาศในโค้ดร่วม) และ actual (implement สำหรับแพลตฟอร์มเฉพาะ) นี่เป็นกลไกในเวลาคอมไพล์: สำหรับ iOS จะคอมไพล์ implement actual จาก iOS sourceSet สำหรับ Android — จาก Android sourceSet implement ที่ไม่ใช่เป้าหมายจะไม่ถูกคอมไพล์
// 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
เมื่อพัฒนาไลบรารีเนทีฟผ่าน Android NDK จะใช้พรีโปรเซสเซอร์ C/C++ แบบคลาสสิกกับไดเรกทีฟ #ifdef, #ifndef, #define ต่างจาก Swift ตรงที่พรีโปรเซสเซอร์ C ทำงานที่ระดับข้อความ — โค้ดในสาขาที่ไม่ได้ทำงานอาจไม่ถูกต้องตามไวยากรณ์
NDK กำหนดแมโครสำหรับแต่ละแพลตฟอร์ม: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux) สำหรับสถาปัตยกรรม: __arm__, __aarch64__, __x86_64__ แมโครเหล่านี้ถูกตั้งค่าโดยอัตโนมัติโดยคอมไพเลอร์เมื่อสร้างสำหรับแพลตฟอร์มเป้าหมาย
// โค้ดเนทีฟสําหรับ 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/B | Java/Kotlin | BuildConfig.FLAVOR |
แอนติแพตเทิร์นที่อันตรายที่สุด — การแพร่กระจายของไดเรกทีฟทั่วโค้ด หากทุกไฟล์ที่สองมี #if นั่นเป็นสัญญาณว่าสถาปัตยกรรมต้องการการปรับโครงสร้าง วิธีแก้ไขที่ถูกต้องคือดึงโค้ดแพลตฟอร์มไปไว้หลังโปรโตคอล/อินเทอร์เฟซและใช้ Dependency Injection
คำถามที่พบบ่อย
Conditional Compilation ทำงาน ในเวลาคอมไพล์: โค้ดที่ไม่ได้ใช้งานจะไม่เข้าไปในไบนารี การตรวจสอบในรันไทม์ (if / switch) จะถูกคอมไพล์เสมอ เงื่อนไขจะถูกตรวจสอบระหว่างการทำงาน วิธีแรกปลอดภัยกว่าและมีประสิทธิภาพกว่า วิธีที่สองยืดหยุ่นกว่า (สามารถเปลี่ยนได้โดยไม่ต้องสร้างใหม่)
ได้ Swift อนุญาตให้ใช้ไดเรกทีฟ #if ภายในฟังก์ชัน ลูป และแม้แต่ภายในนิพจน์ นี่เป็นหนึ่งในคุณสมบัติที่ขาดหายไปใน Swift เวอร์ชันแรกๆ ตัวอย่าง: let x = #if DEBUG 1 #else 0 #endif — โค้ดที่ถูกต้อง
นักพัฒนา Kotlin จงใจปฏิเสธพรีโปรเซสเซอร์ โดยถือว่ามันเป็นแหล่งของ โค้ดที่เปราะบาง แต่พวกเขาเสนอ expect/actual (ความปลอดภัยในเวลาคอมไพล์) และ sourceSets ของ Gradle (การแยกที่ระดับไฟล์) ทั้งสองวิธีมีความน่าเชื่อถือมากกว่าการแทนที่ข้อความ
สร้างแอปพลิเคชันด้วยชุดค่าผสมของแฟลกต่างๆ ใน CI สำหรับ Swift: กำหนดค่า Xcode schemes แยกต่างหากด้วย Active Compilation Conditions ที่แตกต่างกัน สำหรับ Android: กำหนดค่า Build Variants แยกต่างหากและรันการทดสอบสำหรับแต่ละรายการ ระบบอัตโนมัติเป็นสิ่งจำเป็น
ใน Swift เงื่อนไข #if เป็นไดเรกทีฟของคอมไพเลอร์ หากเงื่อนไข本身ไม่ถูกต้องตามไวยากรณ์ (เช่น พิมพ์ชื่อ os() ผิด) คอมไพเลอร์จะแสดง ข้อผิดพลาดในการคอมไพล์ ใน C/C++ พรีโปรเซสเซอร์จะไม่พบแมโครและเงื่อนไขจะกลายเป็นเท็จ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม