Conditional Compilation nelle app mobili — essenza, direttive e principio di funzionamento

Autore: IT Sectr Pubblicato: 2026-06-01 Tempo di lettura: 9 min

Conditional Compilation consente al compilatore di includere o saltare parti del codice sorgente in base a condizioni note al momento della compilazione. Secondo The Swift Programming Language (2026), la direttiva #if viene elaborata durante l'analisi AST prima della generazione del codice macchina. Conditional Compilation offre agli sviluppatori la possibilità di mantenere un'unica base di codice per più piattaforme e configurazioni senza duplicazioni.

Punti chiave

  • Conditional Compilation — tecnica di compilazione selettiva del codice in base a condizioni di piattaforma, configurazione o versione del linguaggio.
  • Direttive #if, #elseif, #else, #endif — le principali costruzioni di compilazione condizionale in Swift, C, C++, Objective-C.
  • Kotlin non ha direttive di preprocessore — invece vengono usati BuildConfig, expect/actual e sourceSets.
  • Vantaggio — il codice per piattaforme non adatte non viene compilato, riducendo la dimensione del binario ed eliminando errori.
  • iOS/macOS codice condiviso — Conditional Compilation è la base dello sviluppo di framework multipiattaforma Apple.

Cos'è Conditional Compilation

Conditional Compilation è un meccanismo in cui il compilatore analizza le direttive di compilazione condizionale e include nel binario di output solo i blocchi di codice le cui condizioni sono soddisfatte. Ciò consente di avere un'unica base di codice che si adatta a diverse piattaforme e configurazioni target.

Il concetto proviene dal C/C++ con le direttive di preprocessore #ifdef, #ifndef, #endif. Nei linguaggi moderni (Swift, Rust, Go), il meccanismo funziona a livello di compilatore senza un preprocessore separato, aumentando la sicurezza: i blocchi condizionali devono essere sintatticamente corretti anche se non vengono compilati.

Secondo la sessione Apple WWDC “Embrace Swift” (2025), circa il 40% dei progetti Swift utilizza la compilazione condizionale per supportare iOS e macOS in un unico target. Per i progetti con UIKit e SwiftUI, il codice dell'interfaccia utente è spesso suddiviso da direttive #if os(iOS) e #if os(macOS), consentendo il riutilizzo della logica di business.

Il vantaggio principale è la sicurezza in fase di compilazione. Il codice per una piattaforma non adatta non solo non viene eseguito, ma non viene compilato. Ciò significa che gli errori nel codice specifico per iOS non si manifesteranno durante la compilazione per macOS e viceversa. I controlli in fase di esecuzione non offrono tali garanzie.

Conditional Compilation in Swift

Swift fornisce quattro direttive chiave: #if, #elseif, #else, #endif. A differenza del preprocessore C, Swift richiede la correttezza sintattica del codice in tutti i rami — il compilatore analizza tutto il codice ma genera codice macchina solo per i rami attivi.

Controlli di piattaforma os()

Swift supporta funzioni di controllo integrate: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Queste funzioni verificano la piattaforma target per cui l'applicazione viene compilata. La combinazione con && e || consente di creare condizioni complesse.

swift
// Codice unificato per iOS, macOS e 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
    }
}

Controlli di versione del compilatore

Swift supporta il controllo della versione del compilatore: #if swift(>=5.9). Ciò è utile per librerie e framework che supportano più versioni di Swift. Le nuove funzionalità del linguaggio (come le macro in Swift 5.9) possono essere protette da tale controllo.

swift
// Retrocompatibilità
#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

Controllo di disponibilità del modulo canImport()

La funzione canImport(ModuleName) verifica se il modulo specificato è disponibile nell'ambiente di compilazione corrente. Questo è il meccanismo più flessibile: non è legato a una piattaforma specifica. Ad esempio, il codice che utilizza CoreHaptics verrà compilato solo sui dispositivi in cui questo framework è disponibile.

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // Implementazione del feedback aptico
        }
    }
#endif

Alternative in Kotlin e Android

Kotlin come linguaggio non ha direttive di preprocessore. Invece, l'ecosistema Android offre tre alternative: campi BuildConfig (controlli in fase di esecuzione), sourceSets (sostituzione di interi file) e expect/actual (in Kotlin Multiplatform).

Source Sets in Gradle

I sourceSets di Gradle consentono di avere diverse implementazioni di classi per diversi flavor o tipi di build. Nella directory src/debug/ si trova l'implementazione per il debug, in src/release/ — per il rilascio. Durante la compilazione, Gradle seleziona il sourceSet appropriato e compila solo i suoi file.

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 in release
    }
}

Expect/Actual in Kotlin Multiplatform

KMP fornisce il meccanismo expect (dichiarazione nel codice comune) e actual (implementazione per una piattaforma specifica). È un meccanismo in fase di compilazione: per iOS viene compilata l'implementazione actual dal sourceSet iOS, per Android — dal sourceSet Android. Le implementazioni non target non vengono compilate.

kotlin
// commonMain — dichiarazione expect
expect fun getPlatformName(): String

// androidMain — actual per Android
actual fun getPlatformName(): String =
    "Android \${Build.VERSION.SDK_INT}"

// iosMain — actual per iOS
actual fun getPlatformName(): String =
    UIDevice.current.systemName() + " " + UIDevice.current.systemVersion

Preprocessore C/C++ e NDK

Nello sviluppo di librerie native tramite Android NDK, viene utilizzato il classico preprocessore C/C++ con le direttive #ifdef, #ifndef, #define. A differenza di Swift, il preprocessore C funziona a livello di testo — il codice nei rami inattivi può essere sintatticamente errato.

Flag di piattaforma NDK

NDK definisce macro per ogni piattaforma: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Per le architetture: __arm__, __aarch64__, __x86_64__. Queste macro vengono impostate automaticamente dal compilatore durante la compilazione per la piattaforma target.

cpp
// Codice nativo per Android e 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

Quando si lavora con NDK, è importante ricordare che il preprocessore C/C++ è una sostituzione di testo. Se c'è un errore sintattico in un ramo inattivo, il compilatore non lo vedrà, ma se un #define errato rompe un ramo attivo — l'errore si manifesterà. Si consiglia di minimizzare le catene di #define e utilizzare costanti constexpr.

Per Rust, che viene anche utilizzato nello sviluppo mobile tramite UniFFI e Mozilla Application Services, esiste il suo meccanismo — i feature flags in Cargo.toml. Flag come #[cfg(target_os = “android”)] in Rust funzionano in modo simile alle direttive Swift: il controllo viene eseguito a livello di compilatore, non di preprocessore. Ciò rende Rust una scelta interessante per librerie native che devono compilare per Android e iOS da un'unica base di codice.

Scenari pratici e antipattern

Conditional Compilation è efficace in scenari strettamente definiti. Se usata in modo errato, crea code smell difficile da testare e mantenere. Esaminiamo gli scenari corretti e gli errori tipici.

Scenari corretti

Il primo scenario è l'astrazione di piattaforma: un'unica Facciata con Conditional Compilation che seleziona l'implementazione di piattaforma all'interno. Il secondo è il debug e profilazione: strumenti di sviluppo che non dovrebbero finire nella release. Il terzo è la retrocompatibilità: supporto per versioni precedenti del sistema operativo fino all'aggiornamento della versione minima.

ScenarioLinguaggioCondizione
Astrazione di piattaformaSwift#if os(iOS)
DebugSwift/ObjC#if DEBUG
RetrocompatibilitàSwift#if swift(>=5.7)
Libreria nativaC/C++#ifdef __ANDROID__
Test A/BJava/KotlinBuildConfig.FLAVOR

Antipattern

L'antipattern più pericoloso è la proliferazione di direttive in tutto il codice. Se ogni secondo file contiene #if, è un segno che l'architettura necessita di refactoring. La soluzione corretta è estrarre il codice di piattaforma dietro protocolli/interfacce e utilizzare l'Iniezione di Dipendenze.

  • #if in ogni file — un antipattern architetturale. Il codice di piattaforma dovrebbe essere isolato dietro protocolli.
  • #if annidati — diventano rapidamente illeggibili. La profondità di annidamento non dovrebbe superare i 2 livelli.
  • Duplicazione di intere funzioni — se una funzione è completamente copiata in #if e #else, dovrebbe essere estratta in una parte comune.
  • Test — il codice all'interno di rami inattivi non viene testato. Sono necessarie build CI di tutte le combinazioni possibili.
  • Flag magici — flag non documentati che il nuovo team di sviluppo non conosce.

Domande frequenti

In che modo Conditional Compilation differisce dai controlli in fase di esecuzione?

Conditional Compilation funziona in fase di compilazione: il codice inattivo non finisce nel binario. I controlli in fase di esecuzione (if / switch) vengono sempre compilati, la condizione viene verificata durante l'esecuzione. Il primo è più sicuro ed efficiente, il secondo è più flessibile (può essere modificato senza ricompilare).

Si può usare #if all'interno di una funzione in Swift?

Sì, Swift consente direttive #if all'interno di funzioni, cicli e persino all'interno di espressioni. Questa è una delle funzionalità che mancavano nelle prime versioni di Swift. Ad esempio: let x = #if DEBUG 1 #else 0 #endif — codice valido.

Perché Kotlin non ha aggiunto un preprocessore?

Gli sviluppatori di Kotlin hanno deliberatamente rifiutato il preprocessore, considerandolo una fonte di codice fragile. Invece, offrono expect/actual (sicurezza in fase di compilazione) e sourceSets di Gradle (isolamento a livello di file). Entrambi gli approcci sono più affidabili della sostituzione di testo.

Come testare il codice all'interno di rami #if inattivi?

Compila l'applicazione con diverse combinazioni di flag nel CI. Per Swift: configura schemi Xcode separati con diverse Active Compilation Conditions. Per Android: configura Build Variants separati ed esegui test per ciascuno. L'automazione è obbligatoria.

Cosa succede se la condizione #if contiene un errore sintattico?

In Swift, la condizione #if è una direttiva del compilatore. Se la condizione stessa è sintatticamente errata (ad esempio, un errore di battitura nel nome os()), il compilatore emetterà un errore di compilazione. In C/C++, il preprocessore semplicemente non troverà la macro e la condizione diventerà falsa.

Riepilogo

  • Conditional Compilation — tecnica di compilazione che esclude il codice non target in fase di compilazione, a differenza dei controlli in fase di esecuzione.
  • Swift supporta #if con os(), canImport(), swift() — direttive sicure in fase di compilazione che richiedono correttezza sintattica di tutti i rami.
  • Kotlin usa expect/actual e sourceSets di Gradle invece di un preprocessore — approcci più affidabili ma meno flessibili.
  • C/C++ in NDK usa il classico preprocessore di testo #ifdef / #ifndef con macro di piattaforma __ANDROID__, __APPLE__.
  • Uso corretto — astrazione di piattaforma, debug, retrocompatibilità. Uso errato — #if in ogni file, annidamento profondo, flag magici.
  • CI è obbligatorio — tutte le combinazioni di flag devono essere compilate e testate automaticamente, altrimenti il codice nei rami inattivi diventa morto.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche