Условната компилация позволява на компилатора да включва или пропуска части от изходния код в зависимост от условия, известни на етапа на изграждане. Според The Swift Programming Language (2026), директивата #if се обработва на етапа на AST анализ преди генериране на машинен код. Условната компилация дава на разработчиците възможност да поддържат единна кодова база за множество платформи и конфигурации без дублиране.
Основни неща
Условната компилация е механизъм, при който компилаторът анализира директивите за условна компилация и включва в изходния бинарен файл само онези блокове код, за които условията са изпълнени. Това позволява наличието на единна кодова база, която се адаптира към различни целеви платформи и конфигурации.
Концепцията идва от 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("Модерен SwiftUI")
}
}
#else
struct ModernView: View {
var body: some View {
Text("Наследен 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).
Gradle sourceSets позволяват наличието на различни реализации на класове за различни варианти или типове изграждане. В директорията src/debug/ се намира реализацията за debug, в src/release/ — за 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 в release
}
}
KMP предоставя механизма expect (декларация в общия код) и actual (реализация за конкретна платформа). Това е механизъм по време на компилация: за iOS се компилира actual реализация от iOS sourceSet, за Android — от Android sourceSet. Нецелевите реализации не се компилират.
// 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
При разработка на native библиотеки чрез Android NDK се използва класическият C/C++ препроцесор с директиви #ifdef, #ifndef, #define. За разлика от Swift, C препроцесорът работи на текстово ниво — кодът в неактивни клонове може да бъде синтактично неправилен.
NDK дефинира макроси за всяка платформа: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). За архитектури: __arm__, __aarch64__, __x86_64__. Тези макроси се задават автоматично от компилатора при изграждане за целевата платформа.
// Native код за 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, който също се използва в мобилната разработка чрез UniFFI и Mozilla Application Services, съществува собствен механизъм — feature flags в Cargo.toml. Флагове като #[cfg(target_os = "android")] в Rust работят подобно на Swift директивите: проверката се извършва на ниво компилатор, а не на ниво препроцесор. Това прави Rust привлекателен избор за native библиотеки, които трябва да се компилират за Android и iOS от една кодова база.
Условната компилация е ефективна в строго определени сценарии. При неправилна употреба създава код с лоша миризма, който е труден за тестване и поддръжка. Нека разгледаме правилните сценарии и типичните грешки.
Първи сценарий — платформена абстракция: единна фасада, вътре в която условната компилация избира платформената реализация. Втори — отстраняване на грешки и профилиране: инструменти за разработчици, които не трябва да попадат в изданието. Трети — обратна съвместимост: поддръжка на стари версии на операционната система, докато не се актуализира минималната версия.
| Сценарий | Език | Условие |
|---|---|---|
| Платформена абстракция | Swift | #if os(iOS) |
| Отстраняване на грешки | Swift/ObjC | #if DEBUG |
| Обратна съвместимост | Swift | #if swift(>=5.7) |
| Native библиотека | C/C++ | #ifdef __ANDROID__ |
| A/B тестване | Java/Kotlin | BuildConfig.FLAVOR |
Най-опасният антимодел — разпространение на директиви в целия код. Ако всеки втори файл съдържа #if, това е сигнал, че архитектурата изисква рефакториране. Правилното решение — изолиране на платформен код зад протоколи/интерфейси и използване на Dependency Injection.
Често задавани въпроси
Условната компилация работи на етапа на компилация: неактивният код не попада в бинарния файл. Проверките по време на изпълнение (if / switch) винаги се компилират, условието се проверява по време на изпълнение. Първото е по-сигурно и ефективно, второто е по-гъвкаво (може да се променя без прекомпилиране).
Да, Swift позволява директиви #if вътре във функции, цикли и дори вътре в изрази. Това е една от възможностите, която липсваше в ранните версии на Swift. Например: let x = #if DEBUG 1 #else 0 #endif — правилен код.
Разработчиците на Kotlin съзнателно се отказаха от препроцесора, смятайки го за източник на чуплив код. Вместо това предлагат expect/actual (сигурност по време на компилация) и Gradle sourceSets (изолация на ниво файлове). И двата подхода са по-надеждни от текстовата замяна.
Изграждайте приложението с различни комбинации от флагове в CI. За Swift: конфигурирайте отделни Xcode схеми с различни Active Compilation Conditions. За Android: конфигурирайте отделни Build Variants и пускайте тестове за всеки. Автоматизацията е задължителна.
В Swift условието #if е компилаторна директива. Ако самото условие е синтактично неправилно (например правописна грешка в името os()), компилаторът ще издаде грешка при компилация. В C/C++ препроцесорът просто няма да намери макроса и условието ще стане грешно.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.