التجميع الشرطي يسمح للمترجم بتضمين أو تخطي أجزاء من الكود المصدري اعتمادًا على شروط معروفة في وقت البناء. وفقًا لـ The Swift Programming Language (2026)، تتم معالجة التوجيه #if أثناء تحليل AST قبل توليد الكود الآلي. التجميع الشرطي يمنح المطورين القدرة على الحفاظ على قاعدة كود واحدة لمنصات وتكوينات متعددة دون تكرار.
الرئيسية
التجميع الشرطي هو آلية يقوم فيها المترجم بتحليل توجيهات التجميع الشرطي ويضمن في الملف الثنائي الناتج فقط كتل الكود التي تتحقق شروطها. هذا يسمح بوجود قاعدة كود واحدة تتكيف مع منصات وتكوينات مستهدفة مختلفة.
المفهوم جاء من C/C++ مع توجيهات المعالج المسبق #ifdef و #ifndef و #endif. في اللغات الحديثة (Swift, Rust, Go)، تعمل الآلية على مستوى المترجم بدون معالج مسبق منفصل، مما يزيد الأمان: يجب أن تكون الكتل الشرطية صحيحة نحويًا حتى لو لم يتم تجميعها.
وفقًا لجلسة Apple WWDC «Embrace Swift» (2025)، حوالي 40% من مشاريع Swift تستخدم التجميع الشرطي لدعم iOS و macOS في هدف واحد. للمشاريع التي تستخدم UIKit و SwiftUI، غالبًا ما يتم تقسيم كود الواجهة بتوجيهات #if os(iOS) و #if os(macOS)، مما يسمح بإعادة استخدام منطق الأعمال.
الميزة الرئيسية هي الأمان في وقت التجميع. الكود الخاص بمنصة غير مناسبة لا يتم تنفيذه فحسب، بل لا يتم تجميعه. هذا يعني أن الأخطاء في الكود الخاص بـ iOS لن تظهر عند التجميع لـ macOS، والعكس صحيح. فحوصات وقت التشغيل لا توفر مثل هذه الضمانات.
يوفر 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 (فحوصات وقت التشغيل)، sourceSets (استبدال ملفات كاملة)، و expect/actual (في Kotlin Multiplatform).
sourceSets في Gradle تسمح بوجود تطبيقات مختلفة للكلاسات لأنواع بناء أو نكهات مختلفة. في المجلد src/debug/ يوجد التطبيق للتصحيح، في src/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 في الإصدار
}
}
KMP يوفر آلية expect (إعلان في الكود المشترك) و actual (تطبيق لمنصة محددة). هذه آلية في وقت التجميع: لنظام iOS يتم تجميع التطبيق الفعلي من sourceSet iOS، لنظام Android — من sourceSet Android. التطبيقات غير المستهدفة لا يتم تجميعها.
// commonMain — إعلان expect
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، يوجد آليته الخاصة — أعلام الميزات في Cargo.toml. أعلام مثل #[cfg(target_os = "android")] في Rust تعمل بشكل مشابه لتوجيهات Swift: يتم الفحص على مستوى المترجم وليس المعالج المسبق. هذا يجعل Rust خيارًا جذابًا للمكتبات الأصلية التي يجب أن تُجمع لنظامي Android و iOS من قاعدة كود واحدة.
التجميع الشرطي فعال في سيناريوهات محددة بدقة. عند استخدامه بشكل غير صحيح، فإنه يخلق كودًا ذا رائحة كريهة (code smell) يصعب اختباره وصيانته. دعنا نستعرض السيناريوهات الصحيحة والأخطاء النموذجية.
السيناريو الأول — تجريد المنصة: واجهة واحدة يختار داخلها التجميع الشرطي تطبيق المنصة. الثاني — التصحيح والتنميط: أدوات المطور التي لا يجب أن تصل إلى الإصدار. الثالث — التوافق العكسي: دعم إصدارات نظام التشغيل القديمة حتى يتم تحديث الحد الأدنى للإصدار.
| السيناريو | اللغة | الشرط |
|---|---|---|
| تجريد المنصة | Swift | #if os(iOS) |
| التصحيح | Swift/ObjC | #if DEBUG |
| التوافق العكسي | Swift | #if swift(>=5.7) |
| مكتبة أصلية | C/C++ | #ifdef __ANDROID__ |
| اختبار A/B | Java/Kotlin | BuildConfig.FLAVOR |
أخطر نمط مضاد هو انتشار التوجيهات في جميع أنحاء الكود. إذا كان كل ملف ثانٍ يحتوي على #if، فهذه إشارة على أن البنية تحتاج إعادة هيكلة. الحل الصحيح هو إخراج كود المنصة خلف بروتوكولات/واجهات واستخدام حقن التبعيات.
الأسئلة المتكررة
التجميع الشرطي يعمل في وقت التجميع: الكود غير النشط لا يصل إلى الملف الثنائي. فحوصات وقت التشغيل (if / switch) تُجمع دائمًا، يتم التحقق من الشرط أثناء التنفيذ. الأول أكثر أمانًا وكفاءة، والثاني أكثر مرونة (يمكن تغييره دون إعادة بناء).
نعم، Swift يسمح بالتوجيهات #if داخل الدوال والحلقات وحتى داخل التعبيرات. هذه إحدى الميزات التي كانت مفقودة في الإصدارات المبكرة من Swift. على سبيل المثال: let x = #if DEBUG 1 #else 0 #endif — كود صحيح.
مطورو Kotlin رفضوا عمدًا المعالج المسبق، معتبرين إياه مصدرًا للكود الهش. بدلاً من ذلك، يقدمون expect/actual (أمان في وقت التجميع) و sourceSets في Gradle (عزل على مستوى الملفات). كلا النهجين أكثر موثوقية من الاستبدال النصي.
قم ببناء التطبيق مع مجموعات مختلفة من الأعلام في CI. لـ Swift: قم بإعداد مخططات Xcode منفصلة بشروط تجميع نشطة مختلفة. لـ Android: قم بإعداد Build Variants منفصلة وشغّل الاختبارات لكل منها. الأتمتة إلزامية.
في Swift، شرط #if هو توجيه مترجم. إذا كان الشرط نفسه غير صحيح نحويًا (مثل خطأ إملائي في اسم os())، سيصدر المترجم خطأ في التجميع. في C/C++، المعالج المسبق ببساطة لن يجد الماكرو وسيصبح الشرط خاطئًا.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا