Conditional Compilation cho phép trình biên dịch bao gồm hoặc bỏ qua các phần của mã nguồn tùy theo điều kiện được biết tại thời điểm xây dựng. Theo The Swift Programming Language (2026), chỉ thị #if được xử lý trong quá trình phân tích AST trước khi tạo mã máy. Conditional Compilation cho phép các nhà phát triển duy trì một cơ sở mã duy nhất cho nhiều nền tảng và cấu hình mà không trùng lặp.
Những điểm chính
Conditional Compilation là cơ chế trong đó trình biên dịch phân tích các chỉ thị biên dịch có điều kiện và chỉ đưa vào tệp nhị phân đầu ra những khối mã thỏa mãn điều kiện. Điều này cho phép có một cơ sở mã duy nhất thích ứng với các nền tảng và cấu hình mục tiêu khác nhau.
Khái niệm này đến từ C/C++ với các chỉ thị tiền xử lý #ifdef, #ifndef, #endif. Trong các ngôn ngữ hiện đại (Swift, Rust, Go), cơ chế hoạt động ở cấp trình biên dịch mà không có bộ tiền xử lý riêng, giúp tăng tính an toàn: các khối có điều kiện phải đúng cú pháp ngay cả khi chúng không được biên dịch.
Theo phiên Apple WWDC “Embrace Swift” (2025), khoảng 40% dự án Swift sử dụng biên dịch có điều kiện để hỗ trợ iOS và macOS trong một mục tiêu duy nhất. Đối với các dự án sử dụng UIKit và SwiftUI, mã UI thường được chia bằng các chỉ thị #if os(iOS) và #if os(macOS), cho phép tái sử dụng logic nghiệp vụ.
Lợi thế chính là an toàn tại thời điểm biên dịch. Mã cho nền tảng không phù hợp không chỉ không được thực thi mà còn không được biên dịch. Điều này có nghĩa là lỗi trong mã dành riêng cho iOS sẽ không xuất hiện khi xây dựng cho macOS và ngược lại. Kiểm tra thời gian chạy không cung cấp sự đảm bảo như vậy.
Swift cung cấp bốn chỉ thị chính: #if, #elseif, #else, #endif. Không giống như bộ tiền xử lý C, Swift yêu cầu tính đúng đắn về cú pháp của mã trong tất cả các nhánh — trình biên dịch phân tích toàn bộ mã nhưng chỉ tạo mã máy cho các nhánh hoạt động.
Swift hỗ trợ các hàm kiểm tra tích hợp: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Các hàm này kiểm tra nền tảng mục tiêu mà ứng dụng đang được xây dựng. Kết hợp với && và || cho phép tạo các điều kiện phức tạp.
// Mã thống nhất cho iOS, macOS và 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 hỗ trợ kiểm tra phiên bản trình biên dịch: #if swift(>=5.9). Điều này hữu ích cho các thư viện và framework hỗ trợ nhiều phiên bản Swift. Các tính năng ngôn ngữ mới (như macro trong Swift 5.9) có thể được bảo vệ bằng cách kiểm tra như vậy.
// Tương thích ngược
#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
Hàm canImport(ModuleName) kiểm tra xem mô-đun được chỉ định có khả dụng trong môi trường xây dựng hiện tại hay không. Đây là cơ chế linh hoạt nhất: nó không bị ràng buộc với một nền tảng cụ thể. Ví dụ, mã sử dụng CoreHaptics sẽ chỉ được biên dịch trên các thiết bị có framework này khả dụng.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Triển khai phản hồi xúc giác
}
}
#endif
Kotlin như một ngôn ngữ không có chỉ thị tiền xử lý. Thay vào đó, hệ sinh thái Android cung cấp ba giải pháp thay thế: trường BuildConfig (kiểm tra thời gian chạy), sourceSets (thay thế toàn bộ tệp) và expect/actual (trong Kotlin Multiplatform).
SourceSets của Gradle cho phép có các triển khai lớp khác nhau cho các hương vị hoặc loại bản dựng khác nhau. Trong thư mục src/debug/ là triển khai gỡ lỗi, trong src/release/ — triển khai phát hành. Khi xây dựng, Gradle chọn sourceSet phù hợp và chỉ biên dịch các tệp của nó.
// 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 trong bản phát hành
}
}
KMP cung cấp cơ chế expect (khai báo trong mã chung) và actual (triển khai cho một nền tảng cụ thể). Đây là cơ chế tại thời điểm biên dịch: cho iOS, triển khai actual từ sourceSet iOS được biên dịch; cho Android — từ sourceSet Android. Các triển khai không phải mục tiêu không được biên dịch.
// commonMain — khai báo expect
expect fun getPlatformName(): String
// androidMain — actual cho Android
actual fun getPlatformName(): String =
"Android \${Build.VERSION.SDK_INT}"
// iosMain — actual cho iOS
actual fun getPlatformName(): String =
UIDevice.current.systemName() + " " + UIDevice.current.systemVersion
Khi phát triển thư viện gốc thông qua Android NDK, bộ tiền xử lý C/C++ cổ điển với các chỉ thị #ifdef, #ifndef, #define được sử dụng. Không giống Swift, bộ tiền xử lý C hoạt động ở cấp văn bản — mã trong các nhánh không hoạt động có thể không đúng cú pháp.
NDK định nghĩa macro cho mỗi nền tảng: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Cho kiến trúc: __arm__, __aarch64__, __x86_64__. Các macro này được trình biên dịch đặt tự động khi xây dựng cho nền tảng mục tiêu.
// Mã gốc cho Android và 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
Khi làm việc với NDK, điều quan trọng là nhớ rằng bộ tiền xử lý C/C++ là một sự thay thế văn bản. Nếu có lỗi cú pháp trong nhánh không hoạt động, trình biên dịch sẽ không thấy nó, nhưng nếu #define sai phá vỡ nhánh hoạt động — lỗi sẽ xuất hiện. Nên giảm thiểu chuỗi #define và sử dụng hằng số constexpr.
Đối với Rust, cũng được sử dụng trong phát triển di động thông qua UniFFI và Mozilla Application Services, có cơ chế riêng của nó — cờ tính năng trong Cargo.toml. Các cờ như #[cfg(target_os = “android”)] trong Rust hoạt động tương tự như các chỉ thị Swift: việc kiểm tra được thực hiện ở cấp trình biên dịch, không phải bộ tiền xử lý. Điều này làm cho Rust trở thành lựa chọn hấp dẫn cho các thư viện gốc phải biên dịch cho Android và iOS từ một cơ sở mã duy nhất.
Conditional Compilation hiệu quả trong các kịch bản được xác định chặt chẽ. Khi sử dụng không đúng, nó tạo ra code smell khó kiểm tra và bảo trì. Hãy xem xét các kịch bản đúng và các lỗi điển hình.
Kịch bản đầu tiên là trừu tượng hóa nền tảng: một mặt tiền duy nhất với Conditional Compilation chọn triển khai nền tảng bên trong. Thứ hai là gỡ lỗi và phân tích: các công cụ nhà phát triển không nên có trong bản phát hành. Thứ ba là tương thích ngược: hỗ trợ các phiên bản hệ điều hành cũ cho đến khi phiên bản tối thiểu được cập nhật.
| Kịch bản | Ngôn ngữ | Điều kiện |
|---|---|---|
| Trừu tượng hóa nền tảng | Swift | #if os(iOS) |
| Gỡ lỗi | Swift/ObjC | #if DEBUG |
| Tương thích ngược | Swift | #if swift(>=5.7) |
| Thư viện gốc | C/C++ | #ifdef __ANDROID__ |
| Kiểm tra A/B | Java/Kotlin | BuildConfig.FLAVOR |
Phản mẫu nguy hiểm nhất là sự lan tràn chỉ thị khắp mã. Nếu cứ mỗi tệp thứ hai chứa #if, đó là dấu hiệu kiến trúc cần tái cấu trúc. Giải pháp đúng là đưa mã nền tảng ra sau các giao thức/giao diện và sử dụng Dependency Injection.
Các câu hỏi thường gặp
Conditional Compilation hoạt động tại thời điểm biên dịch: mã không hoạt động không đi vào tệp nhị phân. Kiểm tra thời gian chạy (if / switch) luôn được biên dịch, điều kiện được kiểm tra trong quá trình thực thi. Cách đầu tiên an toàn hơn và hiệu quả hơn, cách thứ hai linh hoạt hơn (có thể thay đổi mà không cần xây dựng lại).
Có, Swift cho phép các chỉ thị #if bên trong hàm, vòng lặp và thậm chí bên trong biểu thức. Đây là một trong những tính năng còn thiếu trong các phiên bản đầu của Swift. Ví dụ: let x = #if DEBUG 1 #else 0 #endif — mã hợp lệ.
Các nhà phát triển Kotlin đã cố tình từ chối bộ tiền xử lý, coi nó là nguồn gốc của mã dễ vỡ. Thay vào đó, họ cung cấp expect/actual (an toàn tại thời điểm biên dịch) và sourceSets của Gradle (cô lập ở cấp tệp). Cả hai cách tiếp cận đều đáng tin cậy hơn so với thay thế văn bản.
Xây dựng ứng dụng với các tổ hợp cờ khác nhau trong CI. Cho Swift: cấu hình các lược đồ Xcode riêng biệt với các Active Compilation Conditions khác nhau. Cho Android: cấu hình các Build Variants riêng biệt và chạy kiểm thử cho mỗi cái. Tự động hóa là bắt buộc.
Trong Swift, điều kiện #if là một chỉ thị trình biên dịch. Nếu bản thân điều kiện không đúng cú pháp (ví dụ, lỗi chính tả trong tên os()), trình biên dịch sẽ đưa ra lỗi biên dịch. Trong C/C++, bộ tiền xử lý đơn giản là không tìm thấy macro và điều kiện trở thành sai.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm