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 è 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.
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.
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.
// 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
}
}
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.
// 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
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.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Implementazione del feedback aptico
}
}
#endif
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).
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.
// 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
}
}
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.
// 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
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.
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.
// 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.
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.
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.
| Scenario | Linguaggio | Condizione |
|---|---|---|
| Astrazione di piattaforma | Swift | #if os(iOS) |
| Debug | Swift/ObjC | #if DEBUG |
| Retrocompatibilità | Swift | #if swift(>=5.7) |
| Libreria nativa | C/C++ | #ifdef __ANDROID__ |
| Test A/B | Java/Kotlin | BuildConfig.FLAVOR |
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.
Domande frequenti
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).
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.
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.
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.
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
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.
Leggi anche