Conditional Compilation позволяет компилятору включать или пропускать части исходного кода в зависимости от условий, известных на этапе сборки. По данным The Swift Programming Language (2026), директива #if обрабатывается на этапе AST-анализа до генерации машинного кода. Conditional Compilation даёт разработчикам возможность поддерживать единую кодовую базу для нескольких платформ и конфигураций без дублирования.
Главное
Conditional Compilation — это механизм, при котором компилятор анализирует директивы условной компиляции и включает в выходной бинарный файл только те блоки кода, условия для которых выполняются. Это позволяет иметь единую кодовую базу, которая адаптируется под разные целевые платформы и конфигурации.
Концепция пришла из C/C++ с препроцессорными директивами #ifdef, #ifndef, #endif. В современных языках (Swift, Rust, Go) механизм работает на уровне компилятора без отдельного препроцессора, что повышает безопасность: условные блоки должны быть синтаксически корректными, даже если они не компилируются.
По данным Apple WWDC Session "Embrace Swift" (2025), около 40% Swift-проектов используют conditional compilation для поддержки iOS и macOS в одном таргете. Для проектов с UIKit и SwiftUI код UI часто разделён директивами #if os(iOS) и #if os(macOS), что позволяет переиспользовать бизнес-логику.
Главное преимущество — compile-time safety. Код для неподходящей платформы не просто не выполняется, а не компилируется. Это значит, что ошибки в iOS-специфичном коде не проявятся при сборке для macOS, и наоборот. Runtime-проверки такой гарантии не дают.
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 поля (runtime-проверки), 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 (реализация для конкретной платформы). Это compile-time механизм: для iOS компилируется actual-реализация из iOS sourceSet, для Android — из Android sourceSet. Нецелевые реализации не компилируются.
// commonMain — expect declaration
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, который также используется в мобильной разработке через UniFFI и Mozilla Application Services, существует собственный механизм — feature flags в Cargo.toml. Флаги вида #[cfg(target_os = "android")] в Rust работают аналогично Swift директивам: проверка выполняется на уровне компилятора, а не препроцессора. Это делает Rust привлекательным выбором для нативных библиотек, которые должны компилироваться под Android и iOS из одной кодовой базы.
Conditional Compilation эффективна в строго определённых сценариях. При неправильном использовании она создаёт код с запахом (code smell), который сложно тестировать и поддерживать. Рассмотрим корректные сценарии и типичные ошибки.
Первый сценарий — платформенная абстракция: единый фасад, внутри которого Conditional Compilation выбирает платформенную реализацию. Второй — отладка и профилирование: инструменты разработчика, которые не должны попадать в релиз. Третий — обратная совместимость: поддержка старых версий ОС, пока не обновится минимальная версия.
| Сценарий | Язык | Условие |
|---|---|---|
| Платформенная абстракция | 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 работает на этапе компиляции: неактивный код не попадает в бинарник. Runtime-проверки (if / switch) компилируются всегда, условие проверяется во время выполнения. Первое — безопаснее и эффективнее, второе — гибче (можно менять без пересборки).
Да, Swift допускает директивы #if внутри функций, циклов и даже внутри выражений. Это одна из возможностей, отсутствовавшая в ранних версиях Swift. Например: let x = #if DEBUG 1 #else 0 #endif — корректный код.
Разработчики Kotlin сознательно отказались от препроцессора, считая его источником хрупкого кода. Вместо этого они предлагают expect/actual (compile-time безопасность) и Gradle sourceSets (изоляция на уровне файлов). Оба подхода надёжнее текстовой замены.
Собирать приложение с разными комбинациями флагов в CI. Для Swift: настроить отдельные схемы Xcode с разными Active Compilation Conditions. Для Android: настроить отдельные Build Variants и запускать тесты для каждого. Автоматизация обязательна.
В Swift условие #if — это компиляторная директива. Если само условие синтаксически некорректно (например, опечатка в имени os()), компилятор выдаст ошибку компиляции. В C/C++ препроцессор просто не найдёт макрос и условие станет ложным.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также