لغات البرمجة عملياً الأدلة التقنية

أخطاء شائعة وأسلوب Swift الحديث Idiomatic Swift

شغلنا خمسة عشر خطأ يأتي بها المطورون من Java وPython وTypeScript إلى Swift، كل خطأ بمخرجه الحقيقي وإصلاحه، ثم خطة قراءة مرتبة من المصادر الرسمية.

أخطاء شائعة وأسلوب Swift الحديث Idiomatic Swift

بعد أن صارت مكتبة PriceKit وأداة المحاسبة وخدمة الطلبات في متجر التمور والقهوة مكتوبة بـ Swift، انضم إلى الفريق مطوران جديدان: الأول قضى سنوات في Java، والثاني يكتب Python وTypeScript. وكلاهما مطور جيد، ولكن طلبات الدمج Pull Requests الأولى منهما كانت متشابهة بطريقة لافتة: علامة تعجب بعد كل قيمة اختيارية، وكلاسات في كل مكان، وسعر بالريال من نوع Double، وحلقة تعدل نسخة من المنتج وتظن أنها عدلت المنتج نفسه، وفاتورة تطبع للعميل «Optional("0500000000")». ولا شيء من هذا خطأ في البرمجة بالمعنى العام، وإنما عادات صحيحة في لغة أخرى نقلت كما هي إلى Swift.

وقد يتساءل البعض: لماذا تتكرر نفس الأخطاء من مطورين مختلفين؟ والإجابة أن كل لغة تعلمك افتراضات لا تراها، فمطور Java تعلم أن كل كائن مرجع، وأن null يمكن أن يأتي من أي مكان فيفحصه عندما يتذكر، وأن الأرقام تتحول بين الأنواع وحدها، ومطور Python تعلم أن الأخطاء استثناءات تصعد وحدها، وأن النص يفهرس برقم، ومطور JavaScript تعلم أن الانتظار لا يحجب أحداً لأن هناك خيطاً واحداً. وSwift غيرت هذه الافتراضات كلها عمداً، ووضعت أغلب التغيير في نظام الأنواع، فعندما تنقل عادتك القديمة إما أن يرفضها المترجم، وهذا أفضل الاحتمالات، أو أن تجد طريقة لتسكته، وهنا يبدأ الخطأ الحقيقي.

وهذا المقال هو الأخير في مرحلة لغة Swift للمطورين، وقد جمعنا فيه خمسة عشر خطأ من هذا النوع، وكل خطأ له ملف خاطئ نترجمه ونشغله ونعرض مخرجه الحقيقي، ثم ملف صحيح بالأسلوب الذي تتوقعه Swift ومخرجه. وبعضها يمسكه المترجم قبل أن يعمل الكود، وهذا هو الجيد، وبعضها يوقف البرنامج، وأخطرها ما يعمل بهدوء ويعطي نتيجة خاطئة. وفي آخر المقال خطة قراءة من المصادر الرسمية لمن يريد أن يكمل بعد هذه المرحلة.

وسوف نناقش في هذا المقال ما يلي:

  • طريقة تشغيل الأخطاء، ولماذا بوضع التصحيح Debug.
  • أخطاء الاختياري Optional: القوة بـ !، والاختياري داخل النص، وtry? التي تخفي السبب، والاختياري الضمني Int!.
  • أخطاء الأرقام والنصوص: خلط Int وDouble، والمال بـ Double، وفهرسة النص برقم، وطول النص بالحروف أم بالبايتات.
  • أخطاء القيم والمراجع Value Semantics: الحلقة التي تعدل نسخة، والكلاس المشترك، والإغلاق الذي يمسك self.
  • أخطاء البروتوكولات والتعداد: الدالة في الامتداد التي لا تستدعى، وdefault التي تبتلع الحالة الجديدة.
  • أخطاء التزامن Concurrency: الانتظار الحاجب داخل async، والكلاس المشترك بين المهام.
  • مراجعة طلب دمج كامل قبل وبعد، وقواعد swift-format التي تمسك بعض الأخطاء، وملخص الأسلوب الحديث في Swift، وخطة قراءة من المصادر الرسمية مرتبة.

كيف شغلنا الأخطاء؟ Lab

كل خطأ في مجلد lab/m ملفان أو ثلاثة: NN-wrong.swift وNN-right.swift، وسكربت واحد يترجم كل ملف بوضع Swift 6 ثم يشغله إذا ترجم، ويحفظ رمز الخروج:

for f in $(ls *.swift | sort); do
  echo "===== $f"
  swiftc -swift-version 6 "$f" -o /tmp/prog 2>&1 | grep -E "error|warning|note" | head -6
  if [ -x /tmp/prog ]; then timeout 20 /tmp/prog > /tmp/out 2>&1; code=$?; head -4 /tmp/out; echo "exit=$code"; rm -f /tmp/prog
  else echo "(did not compile)"; fi
done
docker run --rm --cpus 2 --memory 512m -v "$PWD:/lab" -w /lab/m swift:6.4.2-noble bash ../run.sh

واخترنا الترجمة بدون -O، أي وضع التصحيح الذي يعمل به المطور طوال يومه، والسبب أننا رأينا في مقال الأنواع والذاكرة أن وضع الإنتاج يحذف نصوص رسائل التوقف، وفي التشغيل الأول بـ -O خرج الخطأ الأول برمز 141 بدل 132، لأن السكربت كان يمرر المخرج إلى head مباشرة فوصلت إشارة الأنبوب المغلق SIGPIPE قبل أن نقرأ رمز التوقف الحقيقي، فصار السكربت يحفظ المخرج في ملف أولاً ثم يقرؤه. والخريطة التالية تجمع الأخطاء الخمسة عشر بحسب من يمسكها:

خريطة الأخطاء الخمسة عشر في أربعة أعمدة: ما يرفضه المترجم بخطأ ترجمة وهو خلط Int وDouble وفهرسة النص برقم وThread.sleep داخل async والكلاس المشترك بين المهام، وما يحذر منه فقط وهو الاختياري داخل النص، وما يوقف البرنامج وهو القوة ! على nil والاختياري الضمني Int!، وما يعطي نتيجة خاطئة بصمت وهو try? والمال بـ Double وطول النص بالحروف والحلقة على نسخة والكلاس المشترك والإغلاق الذي يمسك self والدالة في الامتداد وdefault التي تبتلع الحالة الجديدة، ومعها usleep داخل async
أخطر الأخطاء ليست التي يرفضها المترجم، وإنما التي تعمل بهدوء

أخطاء الاختياري Optional

1. علامة التعجب ! على قيمة قد تكون فارغة Force Unwrap

القادم من Java اعتاد أن القاموس Map.get يرجع null ثم يفحص أو لا يفحص، وفي Swift يرجع القاموس اختيارياً، فيكتب المطور ! ليتخلص من الاختياري بسرعة:

let stock = ["تمر": 10, "قهوة": 0]
let wanted = "هيل"
let left = stock[wanted]!   // crashes: no such key
print("left:", left)
prog/01-wrong.swift:3: Fatal error: Unexpectedly found nil while unwrapping an Optional value
*** Signal 4: Backtracing from 0x7f1aa33b61ef... done ***
exit=132

والأسلوب الصحيح أن تقول ماذا تريد عندما تغيب القيمة: فرع آخر بـ if let، أو خروج مبكر بـ guard let، أو قيمة افتراضية، والقاموس نفسه يقبل قيمة افتراضية في الفهرس:

if let left = stock[wanted] { print("left:", left) } else { print("unknown product:", wanted) }
print("with default:", stock[wanted, default: 0])
unknown product: هيل
with default: 0

وقاعدة عملية في المراجعة: كل ! في كود الإنتاج يحتاج تعليقاً يشرح لماذا لا يمكن أن تكون القيمة فارغة، فإذا لم تستطع أن تكتب هذا التعليق فلا تكتب !. والاستثناءات المقبولة قليلة، مثل رابط ثابت مكتوب في الكود URL(string: "https://...")!، لأن فشله خطأ مبرمج يظهر في أول تشغيل.

ولماذا صممت Swift الاختياري بهذه الصرامة؟ لأن القيمة الفارغة التي يمكن أن تكون في أي مرجع هي ما سماه Tony Hoare، الذي أدخلها في لغة ALGOL W عام 1965، خطأ المليار دولار، لكثرة ما سببته من أعطال وثغرات. وSwift حلتها بأن جعلت الغياب جزءاً من النوع، فالمترجم يعرف أين قد تغيب القيمة ويجبرك على قرار في كل مكان. والعلامة ! هي الباب الذي يعيد المشكلة كما كانت، ولذلك يعاملها المراجعون كإشارة حمراء.

2. الاختياري داخل النص Optional Interpolation

هذا الخطأ رآه العميل نفسه في رسالة SMS:

struct Customer { let name: String; let phone: String? }
let c = Customer(name: "سارة", phone: "0500000000")
print("SMS to \(c.phone)")
09-wrong.swift:3:17: warning: string interpolation produces a debug description for an optional value; did you mean to make this explicit?
  |                 | |- note: provide a default value to avoid this warning
  |                 | `- note: use 'String(describing:)' to silence this warning
SMS to Optional("0500000000")
exit=0

المترجم حذر، ولكنه تحذير وليس خطأ، والكود ترجم وعمل وطبع الكلمة Optional مع علامات التنصيص. وفي Python أو JavaScript كان سيطبع None أو undefined إذا غابت القيمة، أما هنا فيطبع غلاف الاختياري حتى عندما تكون القيمة موجودة. والإصلاح أن تفتح الاختياري قبل أن تضعه في النص:

if let phone = c.phone { print("SMS to \(phone)") } else { print("no phone for \(c.name)") }
SMS to 0500000000

ولهذا السبب لا تترك التحذيرات تتراكم في المشروع، فهذا التحذير وحده كان سيمنع رسالة غريبة لآلاف العملاء. وإذا أردت أن يفشل البناء عند التحذيرات فاستخدم -warnings-as-errors في إعدادات الهدف، أو الوسم @diagnose الجديد في Swift 6.4 الذي يتحكم في التحذيرات من داخل الكود.

ونفس المشكلة تظهر في مكانين آخرين على السيرفر: السجلات Logs، حيث يكتب سطر مثل logger.info("user \(id)") كلمة Optional في كل سطر فيصعب البحث فيها، وردود JSON التي تبنيها بالنص يدوياً بدل Codable. أما Codable نفسه فيتعامل مع الاختياري بشكل صحيح، فيحذف الحقل أو يكتبه null بحسب الإعداد، ولذلك ابن الردود دائماً بـ Codable وليس بإقحام النصوص.

3. try? التي تخفي السبب

ملف إعدادات الخدمة بـ JSON، والمطور القادم من Python كتب قراءته في سطر واحد:

import Foundation
struct Config: Decodable { let port: Int; let vatPercent: Int }
let json = #"{"port": 8080, "vat_percent": 15}"#
let cfg = try? JSONDecoder().decode(Config.self, from: Data(json.utf8))
print("config:", cfg as Any)        // nil, and the reason is gone
config: nil

النتيجة nil، ولا شيء آخر. هل الملف غير موجود؟ هل JSON غير صحيح؟ هل حقل ناقص؟ لا نعرف. والأسلوب الصحيح أن تلتقط الخطأ وتطبعه أو ترميه إلى من يستطيع أن يقرر:

do {
    let cfg = try JSONDecoder().decode(Config.self, from: Data(json.utf8))
    print(cfg)
} catch {
    print("config error:", error)
    let d = JSONDecoder(); d.keyDecodingStrategy = .convertFromSnakeCase
    print("with convertFromSnakeCase:", try! d.decode(Config.self, from: Data(json.utf8)))
}
config error: DecodingError.keyNotFound: Key 'vatPercent' not found in keyed decoding container. Debug description: No value associated with key CodingKeys(stringValue: "vatPercent", intValue: nil) ("vatPercent").
with convertFromSnakeCase: Config(port: 8080, vatPercent: 15)

والرسالة تقول السبب بدقة: الحقل في Swift اسمه vatPercent وفي JSON اسمه vat_percent، وCodable يطابق الأسماء حرفياً، والإصلاح سطر واحد هو استراتيجية convertFromSnakeCase، أو مفاتيح CodingKeys مكتوبة يدوياً. ولاحظ أن try! في السطر الأخير مقبولة هنا فقط لأنها تجربة، وفي كود حقيقي تكون try عادية.

ومتى تكون try? مناسبة؟ عندما يكون الفشل والغياب شيئاً واحداً فعلاً من وجهة نظر الكود، مثل قراءة قيمة من ذاكرة مؤقتة Cache إذا وجدت، أو تحويل نص إلى رقم من مدخل اختياري لا يهمك لماذا فشل. أما قراءة الإعدادات والاتصال بقاعدة البيانات واستدعاء خدمة الدفع فكلها عمليات يحتاج من يقرأ السجلات بعد منتصف الليل أن يعرف سبب فشلها بالضبط.

4. الاختياري الضمني Implicitly Unwrapped Optional: Int!

من يقرأ كود iOS القديم يرى خصائص مثل @IBOutlet var label: UILabel!، فيظن أن ! بعد النوع طريقة مريحة لقيمة «تضبط لاحقاً»، ثم يستخدمها في خدمة على السيرفر:

final class OrderService {
    var vatPercent: Int!                 // "set later", like an IBOutlet
    func total(_ subtotal: Int) -> Int { subtotal + (subtotal * vatPercent + 50) / 100 }
}
let service = OrderService()
print(service.total(2970))           // configure() was never called
p/15-wrong.swift:3: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
*** Signal 4: Backtracing from 0x7f5f66f9e1ef... done ***
exit=132

النوع Int! اختياري حقيقي، ولكن المترجم يفتحه بالقوة في كل مكان يستخدم فيه، فهو علامة ! مخفية في كل سطر يقرأ الخاصية. وفي واجهات iOS له سبب، لأن النظام يربط العناصر بعد إنشاء الكائن وقبل أي استخدام، أما في كودك فهو طريقة لتأجيل سؤال «ما الذي يحتاجه هذا الكائن ليعمل؟». والأسلوب الصحيح أن تجيب على السؤال في دالة البناء:

final class OrderService {
    let vatPercent: Int
    init(vatPercent: Int) { self.vatPercent = vatPercent }   // no service without its configuration
    func total(_ subtotal: Int) -> Int { subtotal + (subtotal * vatPercent + 50) / 100 }
}
let service = OrderService(vatPercent: 15)
print(service.total(2970))
3416

والنتيجة 3416 هللة، أي 34.16 ريالاً، وهو نفس الرقم الذي رأيناه في كل مقالات المرحلة. ولا يمكن الآن أن يوجد OrderService بدون نسبة الضريبة، فالخطأ الذي كان يظهر وقت التشغيل صار مستحيلاً بالتصميم. وقاعدة NeverUseImplicitlyUnwrappedOptionals في swift-format تمسك هذا النوع، وقد قالت عن الملف الخاطئ: use 'Int' or 'Int?' instead of 'Int!'.

والصورة التالية تجمع طرق التعامل مع الاختياري، ومتى تستخدم كل طريقة:

مخطط قرار للتعامل مع قيمة اختيارية: إذا كان غيابها يعني فرعاً آخر في نفس المكان فاستخدم if let، وإذا كان يعني أن الدالة لا تستطيع أن تكمل فاستخدم guard let مع return أو throw، وإذا كانت لها قيمة افتراضية معقولة فاستخدم ?? أو dict[key, default:]، وإذا كنت تمر عبر سلسلة خصائص قد تغيب فاستخدم optional chaining مع ?، وإذا كانت القيمة شرطاً لوجود الكائن فضعها في init بدل Int!، وإذا كان غيابها خطأ مبرمج مستحيلاً ومشروحاً في تعليق فقط فاستخدم !، ومع كل طريقة مثال من المتجر
ست طرق للتعامل مع الاختياري، والقوة بـ ! آخرها وأقلها استخداماً

أخطاء الأرقام والنصوص Numbers and Strings

5. خلط Int مع Double في عملية واحدة

let priceHalalas = 990
let withVat = priceHalalas * 1.15
print(withVat)
02-wrong.swift:2:28: error: binary operator '*' cannot be applied to operands of type 'Int' and 'Double'
  |                            `- note: overloads for '*' exist with these partially matching parameter lists: (Double, Double), (Duration, Double), (Int, Int)
(did not compile)

في Java وPython وJavaScript يتحول العدد الصحيح إلى عشري تلقائياً، أما Swift فلا تحول أي نوع رقمي إلى آخر بدون طلب صريح، والرسالة تقول ذلك وتعرض الأشكال الموجودة للضرب. وأول ما يفعله المطور الجديد أن يكتب Double(priceHalalas) * 1.15، فيترجم الكود، ولكنه دخل في الخطأ التالي. والصحيح في المال أن يبقى الحساب كله بأعداد صحيحة:

let priceHalalas = 990
let vat = (priceHalalas * 15 + 50) / 100   // integer math, rounded half up
print(priceHalalas + vat, "halalas")
1139 halalas

ولماذا لا تحول Swift الأرقام تلقائياً؟ لأن كل تحويل له ثمن مخفي: Double لا يمثل كل عدد صحيح بدقة بعد 2 أس 53، والتحويل من Int64 إلى Int32 قد يتجاوز الحد، والتحويل من عشري إلى صحيح يحذف الجزء العشري. واللغات التي تحول تلقائياً تخفي هذه الأثمان في سطور تبدو بريئة، أما Swift فتجعلك تكتب التحويل، فتراه في المراجعة وتسأل عنه.

6. المال بـ Double

let unit = 9.90
let total = unit * 3
print(total, total == 29.70)
29.700000000000003 false
exit=0

البرنامج عمل بدون أي تحذير، والنتيجة ليست 29.70، والمقارنة بها خاطئة. والسبب أن 9.90 لا يمكن تمثيلها بدقة في النظام الثنائي كما شرحنا في Floating Point من الداخل، وهذه ليست مشكلة Swift، وإنما مشكلة كل لغة، ولكن Swift تجعلها أوضح لأنها تطبع الرقم كاملاً. والإصلاح إما الهللات بأعداد صحيحة، أو النوع Decimal من Foundation الذي يحسب بالنظام العشري:

import Foundation
let unitHalalas = 990
print(unitHalalas * 3, unitHalalas * 3 == 2970)
let d = Decimal(string: "9.90")! * 3
print(d, d == Decimal(string: "29.70")!)
2970 true
29.7 true

ولاحظ أننا أنشأنا Decimal من نص وليس من الرقم 9.90، لأن Decimal(9.90) يبدأ من القيمة العشرية الثنائية غير الدقيقة نفسها. وفي هذا المسار كله نستخدم الهللات، لأن قاعدة البيانات تخزنها كأعداد صحيحة، والضريبة 15% تحسب بالتقريب للأعلى بدون أي كسور عشرية.

والاختيار بين الهللات وDecimal يعتمد على ما حولك: الهللات أسرع وأبسط وتطابق عموداً int في PostgreSQL بلا أي تحويل، ولكنها تفترض أن أصغر وحدة هي الهللة، فإذا احتجت أسعاراً بثلاث خانات عشرية مثل أسعار الوقود أو أسعار الصرف فـ Decimal مع عمود numeric أنسب. والممنوع في الحالتين هو Double.

7. فهرسة النص برقم String Indexing

let name = "قهوة خولاني"
print(name[0])
04-wrong.swift:2:7: error: 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.
Swift.String.subscript:3:10: note: 'subscript(_:)' has been explicitly marked unavailable here
(did not compile)

الرسالة لطيفة: الفهرسة برقم غير متاحة عمداً، واستخدم String.Index. والسبب ما رأيناه في المقال الأول: الحرف في Swift هو ما يراه الإنسان حرفاً، وقد يكون بايتين أو أربعة أو أكثر، فالوصول إلى الحرف رقم 1000 يحتاج المرور على كل ما قبله، واللغة ترفض أن تخفي هذه التكلفة خلف صيغة تبدو رخيصة. والصحيح أن تستخدم ما يعبر عن قصدك:

print(name.first!, name.prefix(4), name[name.index(name.startIndex, offsetBy: 5)])
ق قهوة خ

وfirst وprefix وdropFirst وsplit تكفي في أغلب الحالات، فإذا وجدت نفسك تحسب الفهارس كثيراً فغالباً تحتاج أن تحول النص إلى مصفوفة Array(name) مرة واحدة، أو أن تعمل على utf8 إذا كان النص بروتوكولاً وليس كلاماً.

وهذا مهم في العربية تحديداً، فالكلمة المكتوبة بالحركات حروفها بعين الإنسان قليلة، ولكن كل حركة فيها نقطة ترميز Code Point منفصلة، وSwift تجمع الحرف مع حركاته في حرف واحد Grapheme Cluster كما يشرح فصل Strings and Characters، بينما Java وJavaScript تعدان وحدات UTF-16، فتعطي نفس الكلمة طولاً مختلفاً في كل لغة. فإذا كانت لديك خدمة بلغتين تقارنان طول نص عربي فتأكد أنهما تعدان بنفس الوحدة.

8. طول النص بالحروف Characters وحد الحقل بالبايتات Bytes

حقل اسم المرسل في مزود الرسائل محدود بعشرين بايتاً، والمطور فحص الطول بـ count:

let label = "قهوة عربية ☕️"
if label.count <= 20 { print("accepted, count =", label.count, "but bytes =", label.utf8.count) }
accepted, count = 12 but bytes = 26

اثنا عشر حرفاً بعين الإنسان، وستة وعشرون بايتاً في الحقيقة: تسعة حروف عربية كل منها بايتان، ومسافتان، والفنجان مع محدد الشكل ستة بايتات. فالفحص نجح، والمزود قطع الرسالة أو رفضها. والإصلاح أن تقيس بنفس الوحدة التي يقيس بها الطرف الآخر:

let limit = 20
print(label.utf8.count <= limit ? "fits" : "too long: \(label.utf8.count) bytes > \(limit)")
too long: 26 bytes > 20

والقاعدة: count للعرض على الشاشة وحدود الإدخال التي يراها المستخدم، وutf8.count لقواعد البيانات والبروتوكولات وحقول البايتات، وutf16.count عندما تتعامل مع واجهات JavaScript أو Java التي تعد بوحدات UTF-16.

النص قهوة عربية مع فنجان قهوة مقسوماً إلى 12 حرفاً كما يراها الإنسان، وتحت كل حرف عدد بايتاته في UTF-8: الحروف العربية بايتان والمسافة بايت والفنجان مع محدد الشكل ستة بايتات والمجموع 26، ومع ثلاثة أسطر: count يساوي 12 للشاشة، وutf8.count يساوي 26 لقواعد البيانات والبروتوكولات، وname[0] لا يترجم واستخدم first وprefix وString.Index
الحرف ما يراه الإنسان، والبايتات ما يراه البروتوكول

أخطاء القيم والمراجع Value Semantics

9. الحلقة التي تعدل نسخة for var

المطور القادم من Java أراد أن ينقص قطعة من مخزون كل منتج:

struct Product { let name: String; var stock: Int }
var products = [Product(name: "تمر", stock: 10), Product(name: "هيل", stock: 4)]
for var p in products { p.stock -= 1 }        // compiles: p is a copy
print(products.map(\.stock))
[10, 4]
exit=0

الكود ترجم وعمل، والمخزون لم يتغير. وفي البداية كتب for p in products فرفض المترجم لأن p ثابت، فأضاف var ليسكته، وهنا الفخ: p نسخة من المنتج لأن Product قيمة، والتعديل على النسخة يموت معها في نهاية الدورة. وفي Java كان سيعمل لأن p مرجع إلى الكائن نفسه. والإصلاح أن تعدل العنصر في مكانه بالفهرس، أو أن تبني مصفوفة جديدة:

for i in products.indices { products[i].stock -= 1 }
print(products.map(\.stock))
// or: products = products.map { var p = $0; p.stock -= 1; return p }
[9, 3]

والأسلوب الأوضح في Swift لهذا النوع من التعديل أن تكتب دالة على النوع نفسه بـ mutating، مثل mutating func sell(_ qty: Int)، ثم تستدعيها على العنصر في مكانه products[i].sell(1)، أو أن تمرر المصفوفة إلى دالة بـ inout إذا كانت الدالة يجب أن تعدلها. وكلمتا mutating وinout تقولان للقارئ صراحة أين تتغير القيم، وهذا ما لا تراه في Java حيث كل دالة تستطيع أن تغير أي كائن يصلها.

10. الكلاس المشترك Shared Class حيث تتوقع نسخة

والخطأ المعاكس: سطر سلة جاهز يستخدم كقالب، والمطور يظن أن وضعه في سلتين يعطي سطرين:

final class Line { var qty: Int; init(qty: Int) { self.qty = qty } }
let template = Line(qty: 1)
var cartA = [template]
var cartB = [template]          // "copy" of the array, same object inside
cartB[0].qty = 5
print("cartA:", cartA[0].qty, "cartB:", cartB[0].qty)
cartA: 5 cartB: 5

المصفوفتان قيمتان منفصلتان فعلاً، ولكن العنصر داخل كل منهما مرجع إلى نفس الكائن، فعميل غير كميته في سلته فتغيرت سلة عميل آخر. وهذا الخطأ أخطر في Swift منه في Java، لأن المطور الذي تعلم أن «المصفوفة قيمة تنسخ» يظن أن كل ما فيها ينسخ أيضاً. والإصلاح أن يكون السطر قيمة كما يجب أن يكون:

struct Line { var qty: Int }
cartA: 1 cartB: 5
لوحتان: في اليمين for var p in products حيث p نسخة من كل منتج فيتغير المخزون في النسخة وتبقى المصفوفة 10 و4، والإصلاح for i in products.indices فتصبح 9 و3، وفي اليسار مصفوفتان cartA وcartB كل منهما قيمة ولكن العنصر فيهما مرجع إلى نفس كائن Line فتصبح الكمية 5 في السلتين، والإصلاح struct Line فتبقى 1 في الأولى و5 في الثانية
فخان متعاكسان: نسخة تظنها الأصل، ومرجع تظنه نسخة

11. الإغلاق الذي يمسك self Retain Cycle

final class Checkout {
    var onDone: (() -> Void)?
    let id: Int
    init(id: Int) { self.id = id; onDone = { print("checkout \(self.id) done") } }   // self captured strongly
    deinit { print("Checkout \(id) freed") }
}
do { let c = Checkout(id: 1); c.onDone?() }
print("end of scope")
checkout 1 done
end of scope

لا يوجد سطر «Checkout 1 freed»، أي أن الكائن لم يحرر بعد خروجه من النطاق، لأن الكائن يمسك الإغلاق في onDone، والإغلاق يمسك الكائن عبر self، وهذه الدورة المرجعية التي قسنا ثمنها في المقال الثاني. وفي Java وJavaScript لا مشكلة لأن جامع القمامة يرى الدورة، وفي Swift لا يوجد جامع. والإصلاح قائمة الالتقاط Capture List:

onDone = { [weak self] in print("checkout \(self?.id ?? -1) done") }
checkout 1 done
Checkout 1 freed
end of scope

و[weak self] تجعل self داخل الإغلاق اختيارياً، والأسلوب المعتاد في الإغلاقات الطويلة أن تبدأ بـ guard let self else { return }. ولا تحتاجها في كل إغلاق، فالإغلاق الذي يعمل ويختفي، مثل map وfilter، لا يخزن في أي مكان ولا يصنع دورة، وإنما تحتاجها عندما يخزن الإغلاق في خاصية أو في كائن يعيش طويلاً.

وأين يظهر هذا الخطأ في الواقع؟ في تطبيقات iOS يظهر في المؤقتات Timers ومراقبي الإشعارات والإغلاقات التي تمرر إلى الشاشات، فتبقى الشاشة في الذاكرة بعد إغلاقها، وتكشفه أداة Memory Graph في Xcode. وعلى السيرفر يظهر في الكائنات الطويلة العمر التي تخزن معالجات، مثل عميل يعيد الاتصال تلقائياً ويخزن إغلاقاً يستدعيه عند كل رسالة، فتبقى الاتصالات القديمة حية وتزيد الذاكرة ببطء، كما رأينا مع أداة المحاسبة في المقال الثاني.

أخطاء البروتوكولات والتعداد Protocols and Enums

12. الدالة في الامتداد التي لا تستدعى Static Dispatch

هذا الخطأ يفاجئ حتى من يكتب Swift منذ سنوات:

protocol Receipt { func title() -> String }
extension Receipt {
    func title() -> String { "فاتورة" }
    func footer() -> String { "شكراً لك" }          // not a protocol requirement
}
struct VatReceipt: Receipt {
    func title() -> String { "فاتورة ضريبية" }
    func footer() -> String { "الرقم الضريبي 300000000000003" }
}
let r: any Receipt = VatReceipt()
print(r.title(), "|", r.footer())
فاتورة ضريبية | شكراً لك

العنوان جاء من VatReceipt، والتذييل جاء من الامتداد، رغم أن VatReceipt كتبت footer خاصة بها، والفاتورة الضريبية خرجت بدون الرقم الضريبي. والسبب أن title متطلب في البروتوكول، فاستدعاؤه عبر any Receipt يمر بجدول البروتوكول ويصل إلى تنفيذ النوع الحقيقي، أما footer فليست في البروتوكول، وإنما دالة في الامتداد فقط، فيختار المترجم التنفيذ بحسب النوع المعلن للمتغير، وهو any Receipt، أي تنفيذ الامتداد. وفي Java كل دالة في الواجهة تستدعى ديناميكياً، ولذلك لا يتوقع القادم منها هذا السلوك. والإصلاح أن تضيف الدالة إلى البروتوكول نفسه إذا كانت الأنواع ستغيرها:

protocol Receipt { func title() -> String; func footer() -> String }   // footer is now a requirement
فاتورة ضريبية | الرقم الضريبي 300000000000003

وسبب هذا التصميم الأداء والوضوح معاً: الدالة الموجودة في الامتداد فقط تستدعى استدعاء مباشراً Static Dispatch يعرفه المترجم وقت الترجمة، فيستطيع أن يدمجها في مكان الاستدعاء، بينما متطلبات البروتوكول تمر عبر جدول وقت التشغيل. والقاعدة التي تريحك: إذا كانت الدالة سلوكاً افتراضياً قد تغيره الأنواع فضعها في البروتوكول ثم أعط تنفيذها الافتراضي في الامتداد، وإذا كانت دالة مساعدة مبنية على المتطلبات ولا يجب أن تغيرها الأنواع فاتركها في الامتداد فقط.

13. default التي تبتلع الحالة الجديدة

أضاف الفريق طريقة دفع جديدة هي تابي، وفي دالة حساب الرسوم default قديمة:

enum Payment { case mada, applePay, cash, tabby }    // tabby added last week
func fee(_ p: Payment) -> Int {
    switch p {
    case .mada: return 0
    case .applePay: return 0
    default: return 0                                 // tabby silently lands here
    }
}
print("tabby fee:", fee(.tabby))
tabby fee: 0

الحالة الجديدة دخلت في default بصمت، ورسوم تابي صارت صفراً دون أن يقرر أحد ذلك. وأقوى ما في enum في Swift أن المترجم يفحص شمول switch، وdefault تعطل هذا الفحص. فإذا حذفتها وعالجت الحالات صراحة:

13-right.swift:3:5: error: switch must be exhaustive
  |     |- error: switch must be exhaustive
  |     `- note: add missing case: '.tabby'
(did not compile)

وهذا خطأ ترجمة تريده، لأنه يشير إلى المكان بالضبط ويقول ما الحالة الناقصة، فتكتب القرار صراحة:

case .tabby: return 350          // the compiler pointed here; now the decision is explicit
tabby fee: 350

والقاعدة: لا تكتب default على enum تملكه أنت، واكتبها فقط على تعداد من مكتبة خارجية قد تضيف حالات لا تعرفها، وهناك تستخدم @unknown default التي تبقي التحذير عند ظهور حالة جديدة.

والفرق بين التعدادين مقصود في اللغة، فالمكتبة التي تبنى مع استقرار الواجهة الثنائية مثل مكتبات Apple يمكن أن تضيف حالات في إصدار جديد، ولذلك يجبرك المترجم على حالة احتياطية، بينما تعداد مشروعك أنت مغلق، ويعرف المترجم كل حالاته، فيستطيع أن يحرسها لك ما دمت لم تكتب default.

أخطاء التزامن Concurrency

14. الانتظار الحاجب داخل async

القادم من Java اعتاد Thread.sleep، فكتبها داخل مهام متزامنة:

await withTaskGroup(of: Void.self) { g in
    for _ in 0..<16 { g.addTask { Thread.sleep(forTimeInterval: 0.5) } }   // blocks a pool thread
}
11-wrong.swift:5:42: error: class method 'sleep' is unavailable from asynchronous contexts
(did not compile)

ووضع Swift 6 رفضها، وهذا جيد. ولكن نفس الفكرة بدالة من C لا يعرف المترجم أنها تحجب تمر:

for _ in 0..<16 { g.addTask { usleep(500_000) } }   // C call: the compiler cannot see that it blocks
16 x usleep(0.5 s) on 2 cores: 1.02312618 seconds

والطريقة الصحيحة التي تعلق المهمة ولا تحجب الخيط:

for _ in 0..<16 { g.addTask { try? await Task.sleep(for: .milliseconds(500)) } }   // suspends, frees the thread
16 x Task.sleep(0.5) on 2 cores: 0.520727282 seconds

ست عشرة مهمة كل منها تنتظر نصف ثانية: مع Task.sleep انتهت كلها في نصف ثانية، لأن الانتظار لا يأخذ خيطاً، ومع usleep أخذت ثانية كاملة، أي دفعتين من ثماني مهام، لأن كل مهمة حجبت خيطاً من المجمع وعدد خيوطه ثمانية هنا. وفي مقال التزامن رأينا أن ثماني مهام حاجبة تكفي لتجميد العملية كلها إذا كانت تنتظر بعضها. والانتظار الحقيقي في الخدمات ليس sleep وإنما مكتبة قديمة تقرأ من الشبكة أو الملفات بطريقة حاجبة، فابحث عن نسخة async منها.

وكيف تجدها في مشروع كبير؟ ابحث في الكود عن wait() وsleep وData(contentsOf:) مع الروابط، وFileHandle.readDataToEndOfFile()، وأي دالة من مكتبة C داخل دوال async. وعلى السيرفر بالذات راقب زمن الطلبات البسيطة مثل /health تحت الضغط، فإذا صعد زمنها مع أن عملها لا شيء تقريباً، فهناك من يحجب خيوط المجمع أو حلقات الأحداث.

15. الكلاس المشترك بين المهام Sendable

final class Cart { var total = 0 }
let cart = Cart()
await withTaskGroup(of: Void.self) { g in
    for _ in 0..<4 { g.addTask { cart.total += 1 } }
}
print(cart.total)
14-wrong.swift:4:34: error: non-Sendable type 'Cart' of let 'cart' cannot exit main actor-isolated context [#NonSendableExitingActor]
  |             `- note: class 'Cart' does not conform to the 'Sendable' protocol
14-wrong.swift:4:34: error: main actor-isolated let 'cart' cannot be accessed from outside of the actor
(did not compile)

الكود في أعلى الملف معزول على الممثل الرئيسي، والمتغير cart كلاس غير Sendable، فلا يسمح المترجم أن يخرج إلى مهام تعمل بالتوازي. وهذا ما كان سيصبح سباق بيانات في Java بدون أي تحذير. وأول ما يفعله المطور المستعجل أن يكتب @unchecked Sendable، وقد رأينا في مقال التزامن أنها أضاعت أكثر من نصف التحديثات. والإصلاح الصحيح ممثل:

actor Cart { var total = 0; func add(_ n: Int) { total += n } }
let cart = Cart()
await withTaskGroup(of: Void.self) { g in
    for _ in 0..<4 { g.addTask { await cart.add(1) } }
}
print(await cart.total)
4

ولاحظ أن المترجم في Swift 6 لا يرفض كل كلاس غير Sendable يعبر إلى مهمة، فإذا أنشأت الكائن ثم أرسلته إلى مهمة واحدة ولم تستخدمه بعدها، فالتحليل بالمناطق Regions يقبل ذلك لأنه لا يوجد طرفان. والرفض هنا لأن الكائن نفسه يصل إليه أربع مهام والكود الرئيسي معاً، وهذا بالضبط تعريف الحالة المشتركة التي تحتاج ممثلاً أو قفلاً.

مراجعة طلب دمج كامل Code Review: قبل وبعد

الأخطاء نادراً ما تأتي وحدها، فهذا طلب دمج حقيقي الشكل من المطور القادم من Java، يحسب إجمالي السلة مع الضريبة، وفيه خمس عادات من لغات أخرى في عشرة أسطر:

class CartService {
    var prices: [Int: Double] = [1: 9.90, 2: 45.00]
    func total(items: [(Int, Int)]) -> Double {
        var t = 0.0
        for item in items { t += prices[item.0]! * Double(item.1) }
        return t * 1.15
    }
}
let svc = CartService()
if CommandLine.arguments.count == 1 { print("cart 1:", svc.total(items: [(1, 3)])) }
else { print("cart 2:", svc.total(items: [(1, 3), (9, 1)])) }
cart 1: 34.155
exit=0
p/16-review-before.swift:6: Fatal error: Unexpectedly found nil while unwrapping an Optional value
exit=132

السلة الأولى أعطت 34.155، أي ثلاث خانات عشرية لمبلغ بالريال، وأي تقريب بعدها يقرره من يعرض الرقم، فالشاشة قد تعرض 34.16 والفاتورة 34.15. والسلة الثانية فيها منتج غير موجود فتوقف البرنامج كله، وفي خدمة حقيقية هذا يعني أن طلباً واحداً بمنتج محذوف أسقط العملية التي تخدم كل العملاء. وفي المراجعة نقرأ الكود سطراً بسطر: class بلا هوية ولا حالة تتغير، وvar لأسعار لا تتغير بعد الإنشاء، وDouble للمال، وTuple بلا أسماء فلا يعرف القارئ أي الرقمين الكمية، و! على قاموس، والضريبة تحسب على المجموع العشري ولا تقرب إلى هللة. وبعد المراجعة صار الكود كما يلي:

struct CartService {
    enum Problem: Error { case unknownProduct(Int) }
    struct Totals { let subtotal: Int; let vat: Int; var total: Int { subtotal + vat } }

    let pricesHalalas: [Int: Int]

    func totals(of lines: [(productId: Int, qty: Int)]) throws(Problem) -> Totals {
        var subtotal = 0
        for line in lines {
            guard let price = pricesHalalas[line.productId] else { throw .unknownProduct(line.productId) }
            subtotal += price * line.qty
        }
        return Totals(subtotal: subtotal, vat: (subtotal * 15 + 50) / 100)   // VAT once, on the subtotal
    }
}
subtotal 2970 vat 446 total 3416 halalas
rejected: unknownProduct(9)
exit=0

في الكود الجديد لاحظ التالي:

  • الخدمة struct فيها خاصية let واحدة، فلا يمكن أن يغير أحد الأسعار في منتصف الحساب، وهي Sendable تلقائياً، فتستطيع أن تمررها إلى أي مهمة متزامنة بدون أي كلمة إضافية.
  • المال أعداد صحيحة من أول سطر إلى آخره، والضريبة محسوبة مرة واحدة على المجموع ومقربة للأعلى إلى هللة، فالرقم 3416 هو نفسه في الشاشة والفاتورة والتقرير.
  • المنتج غير الموجود صار خطأ من نوع معروف Problem.unknownProduct(9)، يحوله معالج الطلب إلى رد 400 أو 409، ولا يسقط العملية.
  • أسماء المعاملات totals(of:) وproductId وqty تجعل الاستدعاء يقرأ كجملة، ولا يحتاج القارئ أن يتذكر أي عنصر في الـ Tuple هو الكمية.
  • عدد الأسطر تقريباً نفسه، فالأسلوب الصحيح في Swift ليس أطول، وإنما يضع المعلومات في الأنواع بدل التعليقات والذاكرة.

وطريقة المراجعة نفسها مهمة بقدر الكود، فالمطور الجديد لا يتعلم من قائمة ملاحظات طويلة، وإنما من سؤال واحد في كل ملاحظة: ماذا يقول النوع؟ فبدل «لا تستخدم Double» تكتب «هل يقبل هذا النوع 34.155 ريالاً؟»، وبدل «احذف !» تكتب «ماذا يحدث إذا حذف المنتج من الكتالوج وما زال في سلة عميل؟». وهذه الأسئلة تبني عند المطور العادة الصحيحة، فبعد أسابيع قليلة سوف يسألها لنفسه قبل أن يرسل الطلب، وهذا هو الهدف الحقيقي من المراجعة، وليس إصلاح هذا الطلب وحده.

اجعل الأداة تمسكها: قواعد swift-format Lint Rules

ثمانية من الأخطاء الخمسة عشر لا يمسكها المترجم، والمراجعة البشرية تتعب، ولذلك فإن الخطوة التالية أن تجعل أداة الفحص تمسك ما تستطيع. والمنسق swift-format الذي رأيناه في المقال الأول فيه قواعد معطلة افتراضياً لهذا الغرض، كما في توثيق القواعد، فنفعلها في ملف الإعداد:

{
  "version": 1,
  "indentation": { "spaces": 4 },
  "lineLength": 160,
  "rules": {
    "NeverForceUnwrap": true,
    "NeverUseForceTry": true,
    "NeverUseImplicitlyUnwrappedOptionals": true
  }
}

ثم نفحص الملف الخاطئ الأول ومعه ثلاثة ملفات من الملفات «الصحيحة»:

swift format lint --configuration .swift-format m/01-wrong.swift m/03-right.swift m/10-right.swift m/04-right.swift
swift format lint --strict --configuration .swift-format m/01-wrong.swift
m/01-wrong.swift:3:12: warning: [NeverForceUnwrap] do not force unwrap 'stock[wanted]'
m/01-wrong.swift:3:26: warning: [Spacing] remove 1 space
m/03-right.swift:4:9: warning: [NeverForceUnwrap] do not force unwrap 'Decimal(string: "9.90")'
m/03-right.swift:5:15: warning: [NeverForceUnwrap] do not force unwrap 'Decimal(string: "29.70")'
m/10-right.swift:10:41: warning: [NeverUseForceTry] do not use force try
m/04-right.swift:2:7: warning: [NeverForceUnwrap] do not force unwrap 'name.first'
exit=0
strict exit=1

في المخرج أعلاه لاحظ التالي:

  • الأداة أمسكت الخطأ الأول في مكانه بالضبط، السطر 3 العمود 12، وهو الذي أوقف البرنامج.
  • وأمسكت أيضاً علامات ! وtry! في ملفاتنا «الصحيحة»، وهي علامات وضعناها عمداً لأن الملفات تجارب قصيرة، وهذا بالضبط ما تريده من القاعدة: أن تسألك عن كل واحدة، فإما أن تزيلها أو تعطل القاعدة لذلك السطر وحده بتعليق يشرح السبب.
  • رمز الخروج صفر رغم التحذيرات، ومع --strict صار 1، وهذا ما تضعه في CI حتى يفشل البناء.

وبقية الأخطاء الصامتة، مثل for var والمال بـ Double وdefault على التعداد، لا توجد لها قواعد في swift-format، وبعضها يمكن أن تكتب له قاعدة مخصصة بتعبير نمطي في SwiftLint، وما بقي تمسكه الاختبارات والمراجعة. ولذلك قائمة التحقق في آخر المقال موجهة للمراجع البشري.

الأسلوب الحديث في Swift Idiomatic Swift: ما الذي يجمع هذه الإصلاحات؟

إذا نظرت إلى الإصلاحات الخمسة عشر معاً فستجد أنها كلها تطبيق لبضع أفكار، وهذه الأفكار هي ما يقصده الناس بـ «Swift الحديثة»:

  • اجعل النوع يقول الحقيقة: القيمة التي قد تغيب اختيارية، والحالات المحدودة enum، والمال عدد صحيح بالهللات، والخطأ المتوقع throws بنوع إذا أمكن. وكلما قال النوع أكثر قل ما تفحصه بنفسك.
  • القيم أولاً: struct وlet افتراضياً، وclass وvar عندما تحتاجهما فعلاً، والممثل للحالة المشتركة بين المهام.
  • دع المترجم يعمل: لا default على تعدادك، ولا ! بلا سبب، ولا @unchecked بلا تعليق، ولا تحذيرات متراكمة. فكل واحدة منها مكان أطفأت فيه فحصاً كان سيمسك خطأ.
  • الوضوح عند الاستدعاء: أسماء المعاملات تجعل الاستدعاء جملة، مثل reserve(productId, qty: 3)، وهذا ما تشرحه إرشادات تصميم الواجهات API Design Guidelines، وأهم قاعدة فيها أن الوضوح عند نقطة الاستخدام أهم من الاختصار.
  • البروتوكولات والامتدادات قبل الوراثة: بروتوكول صغير وامتداد يعطي سلوكاً افتراضياً، مع الانتباه إلى أن ما لا يكون متطلباً في البروتوكول لا يستدعى ديناميكياً.
  • التزامن المنظم: async let وTaskGroup، ولا انتظار حاجب داخل async، والإلغاء يفحص في العمل الطويل.

والجدول التالي يجمع الأخطاء وإصلاحاتها في مكان واحد لتستخدمه في مراجعة الكود:

الخطأمن يمسكهالأسلوب الصحيح
dict[key]!توقف وقت التشغيلif let وguard let وdict[key, default:]
var x: Int!توقف وقت التشغيلالقيمة في init كخاصية let
اختياري داخل نصتحذيرافتح الاختياري أولاً أو ?? "..."
try? على عملية مهمةلا أحدdo/catch وطباعة الخطأ
Int * Doubleالمترجمأعداد صحيحة بالهللات
المال بـ Doubleلا أحدهللات أو Decimal(string:)
text[0]المترجمfirst وprefix وString.Index
count لحد بالبايتاتلا أحدutf8.count
for var p in arrayلا أحدfor i in array.indices أو map
كلاس حيث تكفي قيمةلا أحدstruct
إغلاق مخزن يمسك selfلا أحد[weak self]
دالة في امتداد فقطلا أحدأضفها إلى البروتوكول
default على تعدادكلا أحدكل الحالات صراحة، و@unknown default للخارجي
Thread.sleep وusleep في asyncالمترجم للأولى فقطTask.sleep ودوال async
كلاس مشترك بين المهامالمترجم في Swift 6actor أو Mutex

ما الذي لم تغطه هذه المرحلة؟ Beyond the Basics

خمسة مقالات لا تغطي لغة كاملة، وقد اخترنا ما يحتاجه من يبدأ العمل فعلاً، وتركنا أشياء سوف تقابلها قريباً. أولها الماكرو Macros، وقد رأيت منها #expect و@Test، وهي كود يعمل وقت الترجمة ويولد كوداً، وتستطيع أن تكتب ماكرو خاصاً بك. وثانيها بناة النتائج Result Builders، وهي التي تجعل كود SwiftUI يبدو كوصف للواجهة وليس كاستدعاءات دوال، وتستخدم أيضاً في مكتبات توليد HTML وSQL. وثالثها أغلفة الخصائص Property Wrappers التي رأيتها في @Argument و@Option. ورابعها الأنواع العامة المتقدمة، مثل الأنواع المرتبطة Associated Types في البروتوكولات والقيود بـ where. وخامسها التشغيل البيني مع C وC++ وJava، وهو من أكثر ما تطور في الإصدارات الأخيرة. وكلها في الكتاب الرسمي وفي مقترحات Swift Evolution، وخطة القراءة التالية تمر عليها بالترتيب. وإذا كنت تقارن Swift بغيرها من اللغات لمشروع جديد فارجع إلى مقال كيف تختار لغة البرمجة لمشروعك، ففيه نفس خدمة الطلبات كمعيار للمقارنة.

خطة قراءة Reading Plan من المصادر الرسمية

بعد هذه المرحلة أنت تعرف ما يكفي لتعمل، والخطوة التالية أن تقرأ المصادر الرسمية بترتيب يبني كل خطوة على ما قبلها، بدل أن تقفز بين المقالات والفيديوهات. وكلها مجانية ومفتوحة، والترتيب التالي هو ما نقترحه لمن يعرف لغة أخرى:

  1. A Swift Tour في كتاب اللغة، وهي جولة في صفحة واحدة تغطي ما رأيته في المقال الأول وأكثر، اقرأها في ساعة وجرب كل مثال بـ swift في سطر الأوامر.
  2. The Swift Programming Language، الكتاب الرسمي نفسه، ولا تقرأه من أوله إلى آخره، وإنما بهذا الترتيب: The Basics، ثم Strings and Characters، ثم Structures and Classes، ثم Enumerations، ثم Error Handling، ثم Protocols وGenerics، ثم Automatic Reference Counting، ثم Concurrency. وهذه الفصول هي التي تغطي الأخطاء الخمسة عشر في هذا المقال.
  3. API Design Guidelines، صفحة واحدة تشرح كيف تسمي الدوال والمعاملات، وهي ما يجعل كودك يشبه المكتبة القياسية وليس Java مكتوبة بصياغة Swift.
  4. توثيق المكتبة القياسية، وهو موقع التوثيق الجديد الذي أعلن مع Swift 6.4، وارجع إليه عند كل نوع تستخدمه، مثل String وArray وDictionary وResult.
  5. دليل الترحيل إلى Swift 6، حتى لو لم يكن لديك كود قديم، لأنه أوضح شرح لقواعد Sendable والعزل ولماذا يرفض المترجم ما يرفضه.
  6. Swift on Server وأدلته عن البناء والتغليف والنشر والأداء وتسرب الذاكرة على Linux، إذا كان عملك على السيرفر، ومعه توثيق Vapor.
  7. Swift Testing لكتابة الاختبارات بالأسلوب الحديث، ودليل بناء مكتبة بـ SwiftPM لتنظيم الحزم.
  8. Swift Evolution عندما تريد أن تعرف لماذا صممت ميزة بطريقة معينة، فكل ميزة لها مقترح مكتوب فيه المشكلة والبدائل التي رفضت، وقد رأيت روابط بعضها في هذه المرحلة. والنقاشات نفسها في منتديات Swift.
  9. إذا كان هدفك تطبيقات Apple: دروس SwiftUI الرسمية من Apple، وهي تحتاج Xcode على Mac كما ذكرنا في المقال الأول، وكل ما تعلمته عن اللغة في هذه المرحلة ينطبق هناك كما هو.

وإذا أردت خطة زمنية فهذا تقسيم واقعي لمن يعمل بدوام كامل ويقرأ ساعة يومياً: الأسبوع الأول للجولة وفصول الأساسيات والنصوص والهياكل والتعداد، مع إعادة كتابة أداة صغيرة تعرفها من لغتك الحالية بـ Swift. والأسبوع الثاني للأخطاء والبروتوكولات والأنواع العامة وARC، مع اختبارات بـ Swift Testing لكل ما كتبته. والأسبوع الثالث للتزامن ودليل الترحيل، مع تحويل جزء من الأداة إلى مهام متوازية وتشغيلها مع Thread Sanitizer. والأسبوع الرابع للسيرفر أو للتطبيقات بحسب عملك. وأهم من الترتيب أن تكتب كوداً كل يوم وتقرأ رسائل المترجم كاملة، فرسائل Swift الحديثة، كما رأيت في هذا المقال، تشرح المشكلة وتقترح الإصلاح وتعطيك رابطاً إلى صفحة توثيق لكل نوع من الأخطاء.

مسار قراءة بتسع خطوات من المصادر الرسمية: A Swift Tour في ساعة، ثم فصول كتاب The Swift Programming Language بترتيب The Basics وStrings وStructures and Classes وEnumerations وError Handling وProtocols وGenerics وARC وConcurrency، ثم API Design Guidelines، ثم توثيق المكتبة القياسية، ثم دليل الترحيل إلى Swift 6، ثم يتفرع المسار إلى Swift on Server وVapor للسيرفر وإلى دروس SwiftUI على Mac للتطبيقات، مع Swift Testing وSwift Evolution والمنتديات كمراجع دائمة
من الجولة إلى الكتاب إلى الإرشادات، ثم السيرفر أو التطبيقات

مشاكل واجهناها أثناء التجربة Troubleshooting

  • رمز الخروج 141 بدل 132: في التشغيل الأول مررنا مخرج البرنامج إلى head، فأغلق head الأنبوب، وأخذ البرنامج SIGPIPE قبل أن نقرأ رمز التوقف الحقيقي. والحل حفظ المخرج في ملف ثم قراءته.
  • الرسائل غائبة مع -O: نفس الخطأ الأول بوضع الإنتاج توقف بدون «Unexpectedly found nil»، فشغلنا كل الأمثلة بوضع التصحيح لتظهر الرسائل كما يراها المطور.
  • الخطأ الحاجب الذي لا يترجم: أردنا أن نقيس Thread.sleep داخل المهام، فرفضها وضع Swift 6، وهذا بحد ذاته نتيجة، ثم قسنا الفكرة نفسها بـ usleep التي لا يعرف المترجم أنها تحجب.
  • الخطأ 13 يحتاج ملفين صحيحين: الملف الذي يحذف default لا يترجم، وهذا المقصود، فأضفنا ملفاً ثالثاً فيه القرار الصريح.

قائمة تحقق Checklist: مراجعة طلب دمج في Swift

  • هل يوجد ! أو try! أو as! بدون تعليق يشرح لماذا هي آمنة؟
  • هل يوجد try? على عملية يحتاج من يقرأ السجلات أن يعرف سبب فشلها؟
  • هل يوجد Double أو Float في أي حساب مالي؟
  • هل يوجد class لنوع بيانات بلا هوية، أو for var يعدل عناصر مصفوفة؟
  • هل توجد خاصية بنوع T! خارج واجهات iOS، بدل أن تأخذ قيمتها في init؟
  • هل يوجد إغلاق مخزن في خاصية يستخدم self بدون [weak self]؟
  • هل يوجد default في switch على تعداد من نفس المشروع؟
  • هل توجد دالة في امتداد بروتوكول تعيد الأنواع تنفيذها دون أن تكون متطلباً؟
  • هل توجد دالة حاجبة أو @unchecked Sendable داخل كود متزامن؟
  • هل يبنى المشروع بدون تحذيرات، وهل يعمل swift format lint وswift test في CI؟

الخلاصة

وصلنا لنهاية الموضوع ونهاية هذه المرحلة، وأهم ما فيه:

  • من الأخطاء الخمسة عشر رفض المترجم أربعة، وحذر من واحد، وأوقف البرنامج في اثنين، وثمانية عملت بهدوء وأعطت نتيجة خاطئة، ومعها usleep التي مرت من المترجم حيث رفض Thread.sleep، وهذه هي التي تحتاج أن تعرفها.
  • أخطاء الاختياري تصلح بقول ما تريد عند غياب القيمة: if let أو guard let أو قيمة افتراضية، وtry? لا تستخدم حين يهمك السبب.
  • المال أعداد صحيحة، وSwift لا تحول بين الأنواع الرقمية تلقائياً، والنص حروف بعين الإنسان، وحدود البايتات تقاس بـ utf8.count.
  • القيم تنسخ والمراجع تشترك، وfor var تعدل نسخة، والكلاس داخل مصفوفة يبقى مشتركاً، والإغلاق المخزن يحتاج [weak self].
  • ما ليس متطلباً في البروتوكول لا يستدعى ديناميكياً، وdefault على تعدادك تطفئ أقوى فحص في اللغة.
  • في التزامن لا انتظار حاجب داخل async، والحالة المشتركة في ممثل، والمترجم في وضع Swift 6 يمسك أغلب هذه الأخطاء إذا لم تسكته.
  • وفي مراجعة طلب الدمج الكامل اجتمعت خمس عادات في عشرة أسطر، فأعطت السلة 34.155 ريالاً وأسقط منتج محذوف العملية، وبعد المراجعة صار الناتج 3416 هللة وصار المنتج المحذوف خطأ معروف النوع يرد عليه السيرفر بدل أن يتوقف.
  • وخطة القراءة تبدأ بـ A Swift Tour ثم فصول الكتاب الرسمي بترتيب الأخطاء، ثم إرشادات تصميم الواجهات، ثم السيرفر أو التطبيقات.

وهذه بقية مقالات المرحلة، ومقالات تكمل ما رأيناه هنا:

دليلSwift للمطورين: الأدوات والصياغة Syntax على Linuxثبتنا Swift 6.4.2 على Linux وبنينا مكتبة لحساب السعر والضريبة مع أداة سطر أوامر واختبارات، لترى اللغة مقارنة بما تعرفه وتعرف ما يحتاج Mac وما لا يحتاجه.دليلالأنواع والذاكرة ARC والأخطاء Errors في Swiftقسنا النسخ عند الكتابة وتسرب الذاكرة بالدورة المرجعية داخل Container محدود، ورأينا كيف تفشل Swift بـ throws والاختياري والتوقف، لتعرف ما يختلف فيها عن Java وPython وGo.دليلالتزامن في Swift: async/await والممثلون Actorsقسنا مئة طلب HTTP وأربعة ملايين عملية SHA-256 بالتتابع وبالتوازي في Swift، وصنعنا سباق بيانات وجموداً حقيقيين وأصلحناهما، لترى ما يرفضه المترجم وما يبقى عليك.دليلFloating Point من الداخل: كيف يخزن الحاسب الكسوركيف يحفظ معيار IEEE 754 الكسور في البتات، ولماذا تكبر الفجوة بين الأرقام وتضيع خانات المعرفات، ومتى تحتاج النوع العشري.دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.دليلكيف تختار لغة البرمجة Programming Language لمشروعك؟نختار لغة خدمة حقيقية بأسئلة الفريق والتوظيف والمكتبات والرخصة، ثم نكتبها بأربع لغات ونقيس البناء والحجم والذاكرة والطلبات في الثانية بأنفسنا.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتشغيل الأخطاء الخمسة عشر وإصلاحاتها بـ Swift 6.4.2 في swift:6.4.2-noble بوضع Swift 6 داخل Container محدود بمعالجين و512 MB.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.