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 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-проверки такой гарантии не дают.

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 поля (runtime-проверки), sourceSets (замена целых файлов) и expect/actual (в Kotlin Multiplatform).

Source Sets в Gradle

Gradle sourceSets позволяют иметь разные реализации классов для разных флейворов или типов сборки. В директории src/debug/ лежит реализация для debug, в src/release/ — для 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 в release
    }
}

Expect/Actual в Kotlin Multiplatform

KMP предоставляет механизм expect (объявление в общем коде) и actual (реализация для конкретной платформы). Это compile-time механизм: для iOS компилируется actual-реализация из iOS sourceSet, для Android — из Android sourceSet. Нецелевые реализации не компилируются.

kotlin
// 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

Препроцессор 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, который также используется в мобильной разработке через 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/KotlinBuildConfig.FLAVOR

Антипаттерны

Самый опасный антипаттерн — разрастание директив по всему коду. Если каждый второй файл содержит #if, это сигнал, что архитектура требует рефакторинга. Правильное решение — вынести платформенный код за протоколы/интерфейсы и использовать Dependency Injection.

  • #if в каждом файле — архитектурный антипаттерн. Платформенный код должен быть изолирован за протоколами.
  • Вложенные #if — быстро становятся нечитаемыми. Глубина вложенности не должна превышать 2 уровня.
  • Дублирование целых функций — если функция полностью скопирована в #if и #else, её нужно вынести в общую часть.
  • Тестирование — код внутри неактивных веток не тестируется. Необходимы CI сборки всех возможных комбинаций.
  • Магические флаги — undocumented флаги, о которых не знает новая команда разработчиков.

Часто задаваемые вопросы

Чем Conditional Compilation отличается от runtime-проверок?

Conditional Compilation работает на этапе компиляции: неактивный код не попадает в бинарник. Runtime-проверки (if / switch) компилируются всегда, условие проверяется во время выполнения. Первое — безопаснее и эффективнее, второе — гибче (можно менять без пересборки).

Можно ли использовать #if внутри функции в Swift?

Да, Swift допускает директивы #if внутри функций, циклов и даже внутри выражений. Это одна из возможностей, отсутствовавшая в ранних версиях Swift. Например: let x = #if DEBUG 1 #else 0 #endif — корректный код.

Почему Kotlin не добавил препроцессор?

Разработчики Kotlin сознательно отказались от препроцессора, считая его источником хрупкого кода. Вместо этого они предлагают expect/actual (compile-time безопасность) и Gradle sourceSets (изоляция на уровне файлов). Оба подхода надёжнее текстовой замены.

Как тестировать код внутри неактивных веток #if?

Собирать приложение с разными комбинациями флагов в CI. Для Swift: настроить отдельные схемы Xcode с разными Active Compilation Conditions. Для Android: настроить отдельные Build Variants и запускать тесты для каждого. Автоматизация обязательна.

Что будет, если условие #if содержит синтаксическую ошибку?

В Swift условие #if — это компиляторная директива. Если само условие синтаксически некорректно (например, опечатка в имени os()), компилятор выдаст ошибку компиляции. В C/C++ препроцессор просто не найдёт макрос и условие станет ложным.

Итоги

  • Conditional Compilation — техника компиляции, исключающая нецелевой код на этапе сборки, в отличие от runtime-проверок.
  • Swift поддерживает #if с os(), canImport(), swift() — compile-time безопасными директивами, требующими синтаксической корректности всех веток.
  • Kotlin использует expect/actual и Gradle sourceSets вместо препроцессора — более надёжные, но менее гибкие подходы.
  • C/C++ в NDK использует классический текстовый препроцессор #ifdef / #ifndef с платформенными макросами __ANDROID__, __APPLE__.
  • Правильное применение — платформенная абстракция, отладка, обратная совместимость. Неправильное — #if в каждом файле, глубокая вложенность, магические флаги.
  • CI обязателен — все комбинации флагов должны собираться и тестироваться автоматически, иначе код в неактивных ветках становится мёртвым.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также