Kompilasi Bersyarat dalam Aplikasi Seluler — esensi, direktif, dan prinsip kerja

Penulis: IT Sectr Diterbitkan: 2026-06-01 Waktu membaca: 9 mnt

Kompilasi bersyarat memungkinkan kompiler untuk menyertakan atau melewatkan bagian dari kode sumber tergantung pada kondisi yang diketahui pada tahap pembangunan. Menurut The Swift Programming Language (2026), direktif #if diproses pada tahap analisis AST sebelum pembuatan kode mesin. Kompilasi bersyarat memberi pengembang kemampuan untuk mempertahankan basis kode tunggal untuk beberapa platform dan konfigurasi tanpa duplikasi.

Poin Utama

  • Kompilasi bersyarat — teknik kompilasi selektif kode berdasarkan kondisi platform, konfigurasi, atau versi bahasa.
  • Direktif #if, #elseif, #else, #endif — konstruksi dasar kompilasi bersyarat di Swift, C, C++, Objective-C.
  • Kotlin tidak memiliki direktif preprosesor — sebagai gantinya digunakan BuildConfig, expect/actual, dan sourceSets.
  • Keuntungan — kode untuk platform yang tidak sesuai tidak dikompilasi, mengurangi ukuran biner dan menghilangkan kesalahan.
  • iOS/macOS kode bersama — kompilasi bersyarat adalah dasar pengembangan framework lintas platform Apple.

Apa itu Kompilasi Bersyarat

Kompilasi bersyarat adalah mekanisme di mana kompiler menganalisis direktif kompilasi bersyarat dan menyertakan dalam file biner keluaran hanya blok kode yang kondisinya terpenuhi. Ini memungkinkan memiliki basis kode tunggal yang beradaptasi dengan berbagai platform target dan konfigurasi.

Konsep ini berasal dari C/C++ dengan direktif preprosesor #ifdef, #ifndef, #endif. Dalam bahasa modern (Swift, Rust, Go) mekanisme ini bekerja pada tingkat kompiler tanpa preprosesor terpisah, yang meningkatkan keamanan: blok bersyarat harus benar secara sintaksis, bahkan jika tidak dikompilasi.

Menurut data sesi Apple WWDC “Embrace Swift” (2025), sekitar 40% proyek Swift menggunakan kompilasi bersyarat untuk mendukung iOS dan macOS dalam satu target. Untuk proyek dengan UIKit dan SwiftUI, kode UI sering dipisahkan dengan direktif #if os(iOS) dan #if os(macOS), yang memungkinkan penggunaan kembali logika bisnis.

Keuntungan utama — keamanan waktu kompilasi. Kode untuk platform yang tidak sesuai tidak hanya tidak dijalankan, tetapi juga tidak dikompilasi. Ini berarti kesalahan dalam kode spesifik iOS tidak akan muncul saat membangun untuk macOS dan sebaliknya. Pemeriksaan runtime tidak memberikan jaminan seperti itu.

Kompilasi Bersyarat di Swift

Swift menyediakan empat direktif utama: #if, #elseif, #else, #endif. Berbeda dengan preprosesor C, Swift memerlukan kebenaran sintaksis kode di semua cabang — kompiler mengurai semua kode, tetapi menghasilkan kode mesin hanya untuk cabang aktif.

Pemeriksaan Platform os()

Swift mendukung fungsi pemeriksaan bawaan: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Fungsi-fungsi ini memeriksa platform target tempat aplikasi dibangun. Kombinasi dengan && dan || memungkinkan pembuatan kondisi yang kompleks.

swift
// Kode terpadu untuk iOS, macOS, dan 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
    }
}

Pemeriksaan Versi Kompiler

Swift mendukung pemeriksaan versi kompiler: #if swift(>=5.9). Ini berguna untuk pustaka dan framework yang mendukung beberapa versi Swift. Fitur bahasa baru (misalnya, makro di Swift 5.9) dapat dilindungi dengan pemeriksaan semacam itu.

swift
// Kompatibilitas mundur
#if swift(>=5.9)
    @MainActor
    struct ModernView: View {
        var body: some View {
            Text("SwiftUI Modern")
        }
    }
#else
    struct ModernView: View {
        var body: some View {
            Text("SwiftUI Lama")
        }
    }
#endif

Pemeriksaan Ketersediaan Modul canImport()

Fungsi canImport(ModuleName) memeriksa apakah modul yang ditentukan tersedia di lingkungan build saat ini. Ini adalah mekanisme yang paling fleksibel: tidak terikat pada platform tertentu. Misalnya, kode yang menggunakan CoreHaptics hanya akan dikompilasi pada perangkat di mana framework ini tersedia.

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // Implementasi umpan balik taktil
        }
    }
#endif

Alternatif di Kotlin dan Android

Kotlin sebagai bahasa tidak memiliki direktif preprosesor. Sebagai gantinya, ekosistem Android menawarkan tiga alternatif: bidang BuildConfig (pemeriksaan runtime), sourceSets (penggantian seluruh file), dan expect/actual (di Kotlin Multiplatform).

Source Sets di Gradle

Gradle sourceSets memungkinkan memiliki implementasi kelas yang berbeda untuk varian atau tipe build yang berbeda. Di direktori src/debug/ terdapat implementasi untuk debug, di src/release/ — untuk release. Saat membangun, Gradle memilih sourceSet yang sesuai dan hanya mengkompilasi file-file-nya.

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

Expect/Actual di Kotlin Multiplatform

KMP menyediakan mekanisme expect (deklarasi di kode bersama) dan actual (implementasi untuk platform tertentu). Ini adalah mekanisme waktu kompilasi: untuk iOS, implementasi actual dari iOS sourceSet dikompilasi, untuk Android — dari Android sourceSet. Implementasi yang tidak ditargetkan tidak dikompilasi.

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

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

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

Preprosesor C/C++ dan NDK

Dalam pengembangan pustaka native melalui Android NDK, preprosesor C/C++ klasik digunakan dengan direktif #ifdef, #ifndef, #define. Berbeda dengan Swift, preprosesor C bekerja pada tingkat tekstual — kode di cabang tidak aktif mungkin tidak benar secara sintaksis.

Bendera Platform NDK

NDK mendefinisikan makro untuk setiap platform: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Untuk arsitektur: __arm__, __aarch64__, __x86_64__. Makro ini diatur secara otomatis oleh kompiler saat membangun untuk platform target.

cpp
// Kode native untuk Android dan 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

Saat bekerja dengan NDK, penting untuk diingat bahwa preprosesor C/C++ adalah penggantian tekstual. Jika ada kesalahan sintaksis di cabang tidak aktif, kompiler tidak akan melihatnya, tetapi jika karena #define yang salah cabang aktif rusak — kesalahan akan muncul. Disarankan untuk meminimalkan rantai #define dan menggunakan konstanta constexpr.

Untuk Rust, yang juga digunakan dalam pengembangan seluler melalui UniFFI dan Mozilla Application Services, ada mekanisme sendiri — feature flags di Cargo.toml. Bendera seperti #[cfg(target_os = "android")] di Rust bekerja mirip dengan direktif Swift: pemeriksaan dilakukan pada tingkat kompiler, bukan preprosesor. Ini membuat Rust menjadi pilihan menarik untuk pustaka native yang harus dikompilasi untuk Android dan iOS dari satu basis kode.

Skenario Praktis dan Anti-pola

Kompilasi bersyarat efektif dalam skenario yang ditentukan secara ketat. Jika digunakan secara tidak benar, ia menciptakan kode berbau yang sulit diuji dan dipelihara. Mari kita lihat skenario yang benar dan kesalahan umum.

Skenario yang Benar

Skenario pertama — abstraksi platform: fasad tunggal, di mana kompilasi bersyarat memilih implementasi platform. Kedua — debugging dan profiling: alat pengembang yang tidak boleh masuk ke rilis. Ketiga — kompatibilitas mundur: dukungan untuk versi sistem operasi lama hingga versi minimum diperbarui.

SkenarioBahasaKondisi
Abstraksi platformSwift#if os(iOS)
DebuggingSwift/ObjC#if DEBUG
Kompatibilitas mundurSwift#if swift(>=5.7)
Pustaka nativeC/C++#ifdef __ANDROID__
Pengujian A/BJava/KotlinBuildConfig.FLAVOR

Anti-pola

Anti-pola yang paling berbahaya — penyebaran direktif di seluruh kode. Jika setiap file kedua berisi #if, itu adalah sinyal bahwa arsitektur memerlukan refactoring. Solusi yang tepat — memisahkan kode platform di belakang protokol/antarmuka dan menggunakan Dependency Injection.

  • #if di setiap file — anti-pola arsitektural. Kode platform harus diisolasi di belakang protokol.
  • #if bersarang — dengan cepat menjadi tidak terbaca. Kedalaman bersarang tidak boleh melebihi 2 tingkat.
  • Duplikasi seluruh fungsi — jika fungsi sepenuhnya disalin di #if dan #else, itu harus dipindahkan ke bagian bersama.
  • Pengujian — kode di dalam cabang tidak aktif tidak diuji. Build CI dari semua kemungkinan kombinasi diperlukan.
  • Bendera ajaib — bendera tidak terdokumentasi yang tidak diketahui oleh tim pengembang baru.

Pertanyaan yang Sering Diajukan

Apa perbedaan kompilasi bersyarat dengan pemeriksaan runtime?

Kompilasi bersyarat bekerja pada tahap kompilasi: kode tidak aktif tidak masuk ke biner. Pemeriksaan runtime (if / switch) selalu dikompilasi, kondisi diperiksa saat eksekusi. Yang pertama lebih aman dan efisien, yang kedua lebih fleksibel (dapat diubah tanpa membangun ulang).

Bisakah #if digunakan di dalam fungsi di Swift?

Ya, Swift mengizinkan direktif #if di dalam fungsi, loop, dan bahkan di dalam ekspresi. Ini adalah salah satu fitur yang tidak ada di versi awal Swift. Contoh: let x = #if DEBUG 1 #else 0 #endif — kode yang benar.

Mengapa Kotlin tidak menambahkan preprosesor?

Pengembang Kotlin secara sadar menolak preprosesor, menganggapnya sebagai sumber kode rapuh. Sebagai gantinya, mereka menawarkan expect/actual (keamanan waktu kompilasi) dan Gradle sourceSets (isolasi tingkat file). Kedua pendekatan lebih andal daripada penggantian tekstual.

Bagaimana cara menguji kode di dalam cabang #if yang tidak aktif?

Bangun aplikasi dengan kombinasi bendera yang berbeda di CI. Untuk Swift: konfigurasikan skema Xcode terpisah dengan Active Compilation Conditions yang berbeda. Untuk Android: konfigurasikan Build Variants terpisah dan jalankan tes untuk masing-masing. Otomatisasi wajib.

Apa yang terjadi jika kondisi #if mengandung kesalahan sintaksis?

Di Swift, kondisi #if adalah direktif kompiler. Jika kondisi itu sendiri salah secara sintaksis (misalnya, kesalahan ketik pada nama os()), kompiler akan mengeluarkan kesalahan kompilasi. Di C/C++, preprosesor tidak akan menemukan makro dan kondisi akan menjadi salah.

Ringkasan

  • Kompilasi bersyarat — teknik kompilasi yang mengecualikan kode yang tidak ditargetkan pada tahap pembangunan, berbeda dengan pemeriksaan runtime.
  • Swift mendukung #if dengan os(), canImport(), swift() — direktif aman waktu kompilasi yang memerlukan kebenaran sintaksis semua cabang.
  • Kotlin menggunakan expect/actual dan Gradle sourceSets alih-alih preprosesor — pendekatan yang lebih andal tetapi kurang fleksibel.
  • C/C++ di NDK menggunakan preprosesor tekstual klasik #ifdef / #ifndef dengan makro platform __ANDROID__, __APPLE__.
  • Penggunaan yang benar — abstraksi platform, debugging, kompatibilitas mundur. Penggunaan yang salah — #if di setiap file, sarang dalam, bendera ajaib.
  • CI wajib — semua kombinasi bendera harus dibangun dan diuji secara otomatis, jika tidak kode di cabang tidak aktif menjadi mati.

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga