في المقال الأول من هذه المرحلة نقلنا حساب السعر والضريبة في متجر التمور والقهوة إلى مكتبة Swift صغيرة اسمها PriceKit، وبنينا عليها أداة سطر أوامر يستخدمها فريق المحاسبة، وصارت الاختبارات تعمل على Linux. وبعد أسبوعين وصلتك رسالتان في نفس اليوم. الأولى من مطور التطبيق الجديد، ومعها سجل توقف Crash من أجهزة العملاء فيه سطر واحد: Fatal error: Unexpectedly found nil while unwrapping an Optional value، ويسألك: كيف يتوقف تطبيق كامل بسبب قيمة فارغة، أليست Swift لغة «آمنة»؟ والثانية من فريق المحاسبة: الأداة التي تقرأ طلبات الشهر كله وتطابقها مع الفواتير تستهلك ذاكرة تزيد مع كل ألف طلب ولا تنزل، وفي آخر الشهر قتلها النظام وهي في منتصف العمل.
والرسالتان من نفس الموضوع: كيف ترى Swift الأنواع والقيم، ومن يحرر الذاكرة ومتى، وكيف تعبر اللغة عن الفشل. وإذا كنت قادماً من Java أو Python أو Go فهذه الثلاثة بالتحديد هي التي تختلف فيها Swift عما تعرفه، ولذلك سوف نأخذها في هذا المقال بتجارب حقيقية داخل Container محدود بمعالجين و512 MB من الذاكرة، ونقيس كل شيء بدل أن نصفه. وقد شرحنا الصورة العامة لإدارة الذاكرة في اللغات المختلفة في مقال إدارة الذاكرة: يدوياً أم Garbage Collector أم Ownership، وطرق اللغات في التعامل مع الأخطاء في كيف تتعامل لغات البرمجة مع الأخطاء، فلن نكرر النظرية هنا، وإنما نرى كيف تطبقها Swift.
وسوف نناقش في هذا المقال ما يلي:
- نظام الأنواع في Swift: أنواع القيم Value Types وأنواع المراجع Reference Types، ولماذا أغلب ما تكتبه
struct. - النسخ عند الكتابة Copy-on-Write، وكيف تنسخ مصفوفة بعشرة ملايين عنصر في أجزاء من المللي ثانية، وحجم كل نوع في الذاكرة بـ
MemoryLayout. - الاختياري Optional كنوع وليس كقيمة
null، والأنواع العامة Generics والبروتوكولات بـsomeوany. - عداد المراجع ARC من الداخل، والتجربة التي نرى فيها تسرب الذاكرة بالدورة المرجعية Retain Cycle، ثم الإصلاح بـ
weak، وتوقف البرنامج معunowned. - ثمن ARC: خمسة ملايين قيمة كـ
structمقابل خمسة ملايين كائنclass. - الأخطاء:
throwsوtry، والأخطاء ذات النوع Typed Throws، وtry?وResult، والتوقف Traps الذي لا يمكن التقاطه. - ماذا يحدث إذا تسربت الذاكرة داخل Container محدود، والمشاكل التي واجهناها، والأخطاء الشائعة، وقائمة تحقق.
نظام الأنواع Type System في Swift: ثابت وقوي ويستنتج
Swift لغة ثابتة الأنواع Statically Typed، أي أن نوع كل متغير معروف وقت الترجمة ولا يتغير، وقوية الأنواع Strongly Typed، أي أنها لا تحول بين الأنواع تلقائياً أبداً، فلا يجمع Int مع Double ولا يقارن Int مع Int64 إلا بتحويل تكتبه أنت. ومع ذلك فأنت لا تكتب الأنواع في كل سطر، لأن المترجم يستنتجها Type Inference من القيم والاستدعاءات. وقد شرحنا هذه التقسيمات بين اللغات في مقال أنماط البرمجة وأنظمة الأنواع، والسؤال المهم هنا ليس هل الأنواع ثابتة، فهذا تعرفه من Java، وإنما ماذا يعني النوع في Swift عندما تنسخه أو تمرره إلى دالة.
أنواع القيم Value Types وأنواع المراجع Reference Types
في Java كل كائن مرجع، وعندما تكتب b = a فأنت تنسخ عنواناً، ويشير الاثنان إلى نفس الكائن. وفي Python كل شيء كذلك تقريباً. أما في Swift فهناك عائلتان: أنواع القيم، وهي struct وenum والـ Tuple، وتنسخ كاملة عند الإسناد وعند التمرير إلى دالة، وأنواع المراجع، وهي class والـ Actor والإغلاقات Closures، ويشترك فيها كل من يحمل المرجع. والمفاجأة لمن يأتي من Java أن أنواع المكتبة القياسية الأساسية كلها قيم: Int وString وArray وDictionary وSet، ولذلك فإن مصفوفة تمررها إلى دالة لا يمكن أن تتغير من داخل الدالة إلا إذا قلت ذلك صراحة بـ inout. والمثال التالي يبين الفرق:
struct CartLine { var productId: Int; var qty: Int }
final class CartLineRef { var productId: Int; var qty: Int
init(productId: Int, qty: Int) { self.productId = productId; self.qty = qty } }
var a = CartLine(productId: 1, qty: 2)
var b = a // copy
b.qty = 99
print("struct: a.qty=\(a.qty) b.qty=\(b.qty)")
let r1 = CartLineRef(productId: 1, qty: 2)
let r2 = r1 // same object, one more reference
r2.qty = 99
print("class: r1.qty=\(r1.qty) r2.qty=\(r2.qty) same=\(r1 === r2)")نترجم الملف بالتحسينات ونشغله داخل الـ Container:
docker run --rm --cpus 2 --memory 512m -v "$PWD:/lab" -w /lab swift:6.4.2-noble \
bash -c 'swiftc -O values.swift -o /tmp/values && /tmp/values'والمخرج سوف يكون كما يلي:
struct: a.qty=2 b.qty=99
class: r1.qty=99 r2.qty=99 same=trueفي المخرج أعلاه لاحظ التالي:
- مع
structصار لدينا سطران مستقلان في السلة، وتغييرbلم يمسa، وهذا هو السلوك الذي تتوقعه من رقم عادي. - مع
classغيرناr2فتغيرr1، لأنهما نفس الكائن، والمعامل===يقارن الهوية Identity، أي هل هما نفس الكائن في الذاكرة، بينما==يقارن القيمة إذا كان النوع يطبقEquatable. - لاحظ أن
r1وr2معرفان بـletومع ذلك تغيرت خاصية فيهما، لأنletمع الكلاس تعني أن المرجع لا يتغير، وليس الكائن. أماletمعstructفتعني أن القيمة كلها ثابتة، ولن يسمح لك المترجم بتغيير أي خاصية فيها.
والصورة التالية تبين ما يحدث في الذاكرة في الحالتين:

وقد يتساءل البعض: إذا كانت struct تنسخ عند كل إسناد، أليس هذا بطيئاً مع مصفوفة كبيرة؟ والإجابة في الفقرة التالية.
النسخ عند الكتابة Copy-on-Write: لماذا لا يبطئ النسخ؟
المصفوفة في Swift قيمة من الخارج، ولكنها من الداخل تحمل مرجعاً إلى مخزن Buffer في الـ Heap فيه العناصر. وعندما تنسخ المصفوفة فإن Swift تنسخ المرجع فقط وتزيد عداده، فيشترك المتغيران في نفس المخزن، وعندما يحاول أحدهما أن يكتب تسأل اللغة: هل أنا المالك الوحيد لهذا المخزن؟ فإذا كان الجواب لا نسخت المخزن كاملاً ثم كتبت في النسخة، وإذا كان نعم كتبت مباشرة. وهذا ما يسمى النسخ عند الكتابة Copy-on-Write، واختصاراً COW. ولنقس ذلك بمصفوفة فيها عشرة ملايين رقم:
var big = Array(repeating: 1, count: 10_000_000)
var t = clock.now
var other = big // no copy yet
print("assign 10M Ints:", ms(clock.now - t))
t = clock.now
other[0] = 2 // copy happens here
print("first write (copy):", ms(clock.now - t))
t = clock.now
other[1] = 3 // already unique
print("second write:", ms(clock.now - t))
print("big[0]=\(big[0]) other[0]=\(other[0])")
final class Box { var v = 0 }
var box = Box()
print("unique alone:", isKnownUniquelyReferenced(&box))
let keep = box
print("unique with second ref:", isKnownUniquelyReferenced(&box))والمخرج سوف يكون كما يلي:
assign 10M Ints: 0.00831 ms
first write (copy): 198.841662 ms
second write: 0.0003 ms
big[0]=1 other[0]=2
unique alone: true
unique with second ref: falseفي المخرج أعلاه لاحظ التالي:
- الإسناد
var other = bigأخذ ثمانية أجزاء من الألف من المللي ثانية، أي أنه لم ينسخ 80 MB من البيانات، وإنما نسخ مرجعاً وزاد عداداً. - أول كتابة في
otherأخذت قرابة 199 مللي ثانية، وهذا هو النسخ الحقيقي لثمانين مليون بايت، والسبب أنbigما زالت تشترك في المخزن. - الكتابة الثانية أخذت 0.0003 مللي ثانية، لأن
otherصارت تملك مخزنها وحدها. - الدالة
isKnownUniquelyReferencedهي السؤال الذي تسأله المكتبة القياسية قبل كل كتابة، ونراها تعطيtrueللكائن وحده وfalseبعد أن صار له مرجع ثان، وتستطيع أن تستخدمها لتبني COW في أنواعك أنت، كما في توصيات Apple في الاختيار بين struct وclass.
![ثلاث مراحل لمصفوفة بعشرة ملايين رقم: الإسناد var other = big حيث يشير المتغيران إلى نفس المخزن وعداده 2 وأخذ 0.008 مللي ثانية، ثم أول كتابة other[0] = 2 حيث ينسخ المخزن كاملاً 80 MB ويصبح لكل متغير مخزنه وأخذت 198.8 مللي ثانية، ثم الكتابة الثانية other[1] = 3 مباشرة لأن المخزن صار مملوكاً لمتغير واحد وأخذت 0.0003 مللي ثانية](https://arabroot.io/content/images/2026/10/swift-types-memory-02-cow.webp)
والدرس العملي أن النسخ في Swift رخيص حتى تكتب، وأن الكتابة الأولى بعد النسخ قد تكون مكلفة. فإذا كانت لديك دالة تأخذ مصفوفة كبيرة وتعدل نسخة منها في حلقة، فالتكلفة كلها في أول تعديل، وما بعده سريع. والخطأ الذي يقع فيه البعض أن يحتفظ بنسخة قديمة من المصفوفة في متغير لا يحتاجه، فيدفع ثمن النسخ في كل مرة يعدل فيها الأصل.
كم يكلف كل نوع في الذاكرة؟ MemoryLayout
وفي نفس الملف سألنا المترجم عن حجم بعض الأنواع:
MemoryLayout<CartLine>.size = 16
MemoryLayout<CartLineRef>.size = 8 (pointer only)
MemoryLayout<Int?>.size = 9
MemoryLayout<CartLineRef?>.size = 8
MemoryLayout<String>.size = 16في المخرج أعلاه لاحظ التالي:
CartLineفيه رقمانInt، كل واحد 8 بايتات، فحجمه 16 بايت بدون أي رأس Header، وهذا يعني أن مصفوفة من مليون سطر هي 16 مليون بايت متصلة في الذاكرة، تماماً مثل مصفوفة C.CartLineRefحجم المتغير منه 8 بايتات فقط، لأنه مؤشر، والكائن نفسه في الـ Heap ومعه رأس فيه نوع الكائن وعداد المراجع.Int?حجمه 9، أي الرقم وبايت إضافي يقول هل القيمة موجودة. فالاختياري ليس قيمةnullسحرية، وإنما نوع حقيقي يحمل قيمته ومعها علامة.CartLineRef?بقي 8، لأن المترجم يعرف أن المؤشر الصحيح لا يكون صفراً أبداً، فيستخدم الصفر ليعنيnilبدون بايت إضافي.Stringحجمها 16 بايت مهما كان طول النص، لأن النص القصير حتى 15 بايتاً بـ UTF-8 يخزن داخل هذه البايتات نفسها، والأطول يخزن في الـ Heap ويبقى هنا مرجعه.
الاختياري Optional: نوع وليس قيمة null
عرفنا الآن أن Int? نوع حجمه 9 بايتات، والحقيقة أنه اختصار لـ Optional<Int>، وهو enum عادي في المكتبة القياسية له حالتان: .none و.some(value)، كما في توثيق Optional. ولهذا السبب لا يمكن أن تصل قيمة فارغة إلى متغير من نوع Int أبداً، فالنوع Int لا يملك حالة فارغة أصلاً، وكل مكان قد تغيب فيه القيمة يجب أن يقول ذلك في نوعه. وفي Java يمكن أن يكون أي مرجع null، والمترجم لا يعرف أيها قد يكون، ولذلك يظهر NullPointerException وقت التشغيل، وفي TypeScript حلت المشكلة بالخيار strictNullChecks كما رأينا في مقال TypeScript للمطورين، وفي Swift هي جزء من اللغة منذ اليوم الأول.
ولكي تستخدم القيمة داخل الاختياري عليك أن تفتحه Unwrap، ولذلك أكثر من طريقة:
let stock: [Int: Int] = [1: 10, 2: 0]
if let left = stock[3] { print(left) } else { print("unknown product") } // if let
func reserve(_ id: Int) -> Bool {
guard let left = stock[id], left > 0 else { return false } // guard: early exit
return true
}
let left = stock[3] ?? 0 // default value
let name = customer?.address?.city // optional chaining: String?
let forced = stock[3]! // force unwrap: traps if nilوالطريقة الأخيرة، علامة التعجب !، هي مصدر رسالة المطور في بداية المقال، فهي تقول للمترجم «أنا متأكد أن القيمة موجودة»، فإذا كانت فارغة توقف البرنامج. فاللغة آمنة بمعنى أنها لا تقرأ ذاكرة خاطئة ولا تكمل بقيمة غير معرفة، ولكنها لا تمنعك من أن تطلب التوقف بنفسك، وسوف نرى رسالة التوقف بالضبط في قسم الأخطاء.
البروتوكولات Protocols والأنواع العامة Generics: some و any
وآخر ما يجب أن تعرفه عن الأنواع قبل الذاكرة هو الفرق بين some وany، لأنك سوف تراهما في كل كود حديث. فعندما تكتب دالة تأخذ «أي شيء يطبق Priced» فأمامك طريقتان:
func total(_ items: [some Priced]) -> Int // generic: one concrete type, known at compile time
func total(_ items: [any Priced]) -> Int // existential: a box that can hold different typesالأولى some هي اختصار لدالة عامة Generic، والمترجم ينشئ نسخة متخصصة منها لكل نوع تستدعيها به، فتكون الاستدعاءات مباشرة وسريعة، ولكن كل عناصر المصفوفة يجب أن تكون من نفس النوع. والثانية any صندوق Existential يستطيع أن يحمل أنواعاً مختلفة في نفس المصفوفة، ولكن كل استدعاء يمر عبر جدول يبحث فيه عن الدالة وقت التشغيل، وقد يحتاج الصندوق مكاناً في الـ Heap إذا كانت القيمة كبيرة. والقاعدة التي توصي بها اللغة أن تبدأ بـ some، ولا تنتقل إلى any إلا عندما تحتاج فعلاً أن تخلط أنواعاً مختلفة، وهذا موضح في فصل Opaque and Boxed Protocol Types في كتاب اللغة. وفي Swift 6.4 صار بإمكانك أن تكتب some Rocket? بدل (some Rocket)?، وهذه من التحسينات الصغيرة في الإصدار.
من يدير الذاكرة؟ عداد المراجع ARC
الآن نصل إلى رسالة فريق المحاسبة. ففي Java وC# وGo وJavaScript يوجد جامع قمامة Garbage Collector يعمل في الخلفية، يبحث كل فترة عن الكائنات التي لم يعد أحد يصل إليها ويحررها، وفي C تحرر الذاكرة بنفسك، وفي Rust يحدد المترجم وقت التحرير من قواعد الملكية Ownership. أما Swift فاختارت طريقاً رابعاً هو العد التلقائي للمراجع Automatic Reference Counting، واختصاراً ARC: كل كائن من نوع class يحمل عداداً لعدد المراجع القوية إليه، والمترجم يضيف بنفسه زيادة العداد Retain عند كل مرجع جديد، وإنقاصه Release عند انتهاء كل مرجع، وعندما يصل العداد إلى صفر يحرر الكائن فوراً وتعمل دالة deinit. وهذا مشروح بالتفصيل في فصل Automatic Reference Counting في كتاب اللغة.
ولهذا الاختيار نتيجتان تهمانك كمطور سيرفر. الأولى أن التحرير حتمي Deterministic، فلا يوجد توقف مفاجئ لجمع القمامة، والذاكرة تعود فور انتهاء آخر مرجع، وهذا ما ذكره فريق Apple عندما نقل خدمة Password Monitoring من Java إلى Swift، حيث قال إن الإدارة الحتمية للذاكرة جعلت الخدمة تحتاج مئات الميجابايتات لكل نسخة بدل عشرات الجيجابايتات. والثانية أن ARC لا يعرف الدورات، فإذا أشار كائنان كل منهما إلى الآخر فلن يصل أي عداد إلى صفر، وسوف يبقيان في الذاكرة إلى الأبد، وهذا بالضبط ما حدث في أداة المحاسبة.
كيف يعمل ARC من الداخل: retain و release و deinit
لنرى أولاً متى يحرر الكائن. في ملف arc.swift عداد عام alive يزيد في دالة البناء وينقص في deinit:
nonisolated(unsafe) var alive = 0
final class Customer {
let name: String
var orders: [Order] = []
var payload = [UInt8](repeating: 0, count: 1024) // 1 KB so the leak is visible
init(name: String) { self.name = name; alive += 1 }
deinit { alive -= 1 }
}
do {
let c = Customer(name: "سارة")
print("inside scope alive=\(alive)")
_ = c
}
print("after scope alive=\(alive)")والمخرج سوف يكون كما يلي:
inside scope alive=1
after scope alive=0الكائن حرر في نفس اللحظة التي انتهى فيها النطاق do، وليس «في وقت ما لاحقاً». ولاحظ nonisolated(unsafe) قبل المتغير العام، وهذه من قواعد Swift 6: المتغير العام القابل للتغيير يمكن أن يصل إليه أكثر من خيط Thread، ولذلك يعزله المترجم على الممثل الرئيسي ويرفض أن تغيره دالة غير معزولة مثل init وdeinit، إلا إذا قلت صراحة إنك تتحمل المسؤولية، وهذا موضوع المقال الثالث عن التزامن. ولأن هذه التجربة تعمل في خيط واحد فالكلمة هنا مقبولة، ولكن لا تكتبها في كود حقيقي دون أن تفكر. وجربنا ملفاً صغيراً بنفس الفكرة بدون الكلمة بوضع Swift 6، فكان المخرج:
/tmp/g.swift:2:33: error: main actor-isolated var 'alive' can not be mutated from a nonisolated context
| `- note: mutation of this var is only permitted within the actorوكل مرجع قوي جديد يعني زيادة على العداد، وهذه الزيادة عملية ذرية Atomic لأن الكائن قد يصل إليه أكثر من خيط، والعمليات الذرية أغلى من الجمع العادي. والمترجم في وضع -O يحذف كثيراً من هذه الزيادات عندما يثبت أنها غير ضرورية، ولكنه لا يحذفها كلها، وسوف نقيس ثمنها بعد قليل.
التجربة Lab: تسرب الذاكرة بالدورة المرجعية Retain Cycle
في أداة المحاسبة كان كل عميل يحمل مصفوفة طلباته، وكل طلب يحمل مرجعاً إلى عميله، لأن الكود يحتاج اسم العميل عند طباعة الفاتورة. وهذا شكل طبيعي جداً، وهو بالضبط الدورة المرجعية:
final class Order {
let id: Int
var customer: Customer? // strong: creates a cycle
weak var weakCustomer: Customer? // weak: does not keep it alive
init(id: Int) { self.id = id; alive += 1 }
deinit { alive -= 1 }
}
for i in 0..<n {
let c = Customer(name: "عميل \(i)")
let o = Order(id: i)
c.orders.append(o)
if mode == "strong" { o.customer = c } else { o.weakCustomer = c }
}
print("mode=\(mode) after \(n) pairs: alive=\(alive) rss=\(String(format: "%.1f", rssMB())) MB")والدالة rssMB تقرأ الذاكرة المقيمة Resident Set Size للعملية من الملف /proc/self/statm، أي الذاكرة الفعلية التي يحجزها البرنامج في RAM. نشغل الحلقة مئتي ألف مرة بالطريقتين:
swiftc -O arc.swift -o /tmp/arc
/tmp/arc strong
/tmp/arc weakوالمخرج سوف يكون كما يلي:
mode=strong start rss=16.1 MB
mode=strong after 200000 pairs: alive=400000 rss=251.9 MB
mode=weak start rss=16.0 MB
mode=weak after 200000 pairs: alive=0 rss=16.0 MBفي المخرج أعلاه لاحظ التالي:
- في الحلقة لا يبقى أي متغير خارجي يشير إلى العميل أو الطلب، فكل دورة من الحلقة تنتهي ويخرج
cوoمن النطاق، ومع ذلك فإن 400 ألف كائن ما زالت حية مع المرجع القوي، أي مئتا ألف عميل ومئتا ألف طلب، لأن كل عميل يمسك طلبه وكل طلب يمسك عميله، فلا يصل أي عداد إلى صفر. - الذاكرة صعدت من 16 MB إلى 252 MB، أي قرابة 1.2 KB لكل زوج: الكيلوبايت الذي وضعناه في
payload، ورؤوس الكائنين، والمصفوفة، والنص. - مع
weakانتهت الحلقة وعدد الكائنات الحية صفر والذاكرة كما بدأت، لأن المرجع الضعيف لا يزيد العداد، فعندما خرجcمن النطاق صار عداد العميل صفراً فحرر، وحرر معه مصفوفة طلباته، فحرر الطلب. - لا يوجد Garbage Collector يأتي لاحقاً لينقذ الموقف، فالكائنات المسربة في Swift تبقى حتى تنتهي العملية.

المرجع الضعيف weak والمرجع unowned: الفرق وثمنه
في Swift طريقتان لتمنع الدورة من البداية: weak وunowned. فالمرجع weak اختياري دائماً، ويصبح nil تلقائياً عندما يحرر الكائن، ولذلك هو آمن ولكنه يجبرك أن تتعامل مع غياب القيمة. والمرجع unowned ليس اختيارياً، فهو وعد منك بأن الكائن سوف يعيش أطول من المرجع، مثل الفاتورة التي لا توجد بدون عميلها. ولنرى ماذا يحدث إذا لم تف بالوعد:
final class Receipt {
unowned let customer: Customer
weak var weakCustomer: Customer?
init(_ c: Customer) { customer = c; weakCustomer = c }
}
var c: Customer? = Customer(name: "سارة")
let r = Receipt(c!)
c = nil // the last strong reference is gone
print("weak now:", r.weakCustomer as Any) // nil, safely
print("unowned now:", r.customer.name) // the object is gone: trapوالمخرج سوف يكون كما يلي:
Customer سارة freed
weak now: nil
Fatal error: Attempted to read an unowned reference but object 0x55f43a880300 was already destroyed
*** Signal 6: Backtracing from 0x7ff9d271cc0c... done ***
exit=134المرجع الضعيف أعطى nil بهدوء، والمرجع unowned أوقف البرنامج برسالة واضحة ورمز خروج 134، أي الإشارة SIGABRT. ولاحظ أن البرنامج لم يقرأ ذاكرة محررة ويكمل بقيمة عشوائية كما يحدث في C، وإنما توقف، وهذا ما تعنيه Swift بأمان الذاكرة Memory Safety. والقاعدة العملية: استخدم weak افتراضياً، ولا تستخدم unowned إلا عندما يكون عمر الكائنين مرتبطاً فعلاً، ويكون التوقف عند الخطأ أفضل من أن تحمل اختيارياً في كل مكان. وقد ناقشنا لماذا صار أمان الذاكرة مطلباً على مستوى الحكومات في مقال أمان الذاكرة Memory Safety، وSwift من اللغات التي تذكرها تلك التوصيات كلغة آمنة للذاكرة.
والدورات لا تأتي من الكلاسات فقط، فالإغلاق Closure نوع مرجعي، وإذا خزنت إغلاقاً في خاصية واستخدم الإغلاق self فقد صنعت دورة بين الكائن والإغلاق، وهذا أشهر مصدر للتسرب في تطبيقات iOS، وسوف نرى مثاله الحي والإصلاح بـ [weak self] في مقال الأخطاء الشائعة.
كم يكلف ARC؟ struct مقابل class
بعد أن رأينا كيف يعمل ARC بقي سؤال الأداء: ما ثمن الكائنات المرجعية مقارنة بالقيم؟ فكتبنا تجربة تبني خمسة ملايين سعر كـ struct مرة وكـ class مرة، ثم تجمعها، ثم تحرر الكائنات:
struct PS { var price: Int }
final class PC { var price: Int; init(_ p: Int) { price = p } }
var s = [PS](); s.reserveCapacity(n)
for i in 0..<n { s.append(PS(price: i & 1023)) }
var sum = 0; for x in s { sum &+= x.price }
var c = [PC](); c.reserveCapacity(n)
for i in 0..<n { c.append(PC(i & 1023)) }
var sum2 = 0; for x in c { sum2 &+= x.price }
c.removeAll()وشغلناها خمس مرات، لأن أول تشغيل كان فيه ضجيج واضح من الجهاز المضيف:
struct build 0.072403813 seconds sum 0.006830539 seconds (2557420128)
class build 1.202220879 seconds sum 0.220634413 seconds (2557420128)
class free 5M objects 0.662216309 seconds
struct build 0.046407028 seconds sum 0.013620377 seconds (2557420128)
class build 0.840878394 seconds sum 0.231223858 seconds (2557420128)
class free 5M objects 0.634828423 seconds
struct build 0.032148251 seconds sum 0.004469093 seconds (2557420128)
class build 1.083735899 seconds sum 0.232017029 seconds (2557420128)
class free 5M objects 0.557430686 seconds
struct build 0.076148452 seconds sum 0.015942264 seconds (2557420128)
class build 1.353005469 seconds sum 0.436365904 seconds (2557420128)
class free 5M objects 0.525433005 seconds
struct build 0.047808783 seconds sum 0.015070156 seconds (2557420128)
class build 1.050287539 seconds sum 0.201601789 seconds (2557420128)
class free 5M objects 0.5767611 secondsوإذا أخذنا الوسيط Median من كل عمود، أي القيمة التي في المنتصف بعد الترتيب، وهو أقل تأثراً بالتشغيل الشاذ من المتوسط:
| العملية | struct | class | النسبة |
|---|---|---|---|
| بناء 5 ملايين عنصر | 0.048 ثانية | 1.084 ثانية | قرابة 22 ضعفاً |
| جمع الأسعار | 0.014 ثانية | 0.231 ثانية | قرابة 17 ضعفاً |
| تحرير العناصر | مع المصفوفة دفعة واحدة | 0.577 ثانية | خمسة ملايين deinit |

والسبب ليس ARC وحده، وإنما ثلاثة أشياء معاً: كل كائن يحتاج حجزاً منفصلاً من الـ Heap بدل مكان في مخزن واحد متصل، وكل كائن له رأس فيه عداد المراجع، والمرور على المصفوفة يقفز بين عناوين متفرقة في الذاكرة بدل أن يقرأ بايتات متجاورة تستفيد منها ذاكرة المعالج المؤقتة Cache. وهذا ما توصي به إرشادات Swift على السيرفر عن الحجز: قلل الحجوزات في المسارات الساخنة، والطريقة الأسهل لذلك أن تكون أنواعك قيماً. ولاحظ أن الفرق بين التشغيلات كبير، فالجمع مع class أخذ مرة 0.20 ومرة 0.44 ثانية، وهذا ضجيج جهاز تطوير يعمل عليه غيره، ولذلك نقرأ الوسيط ولا نعتمد على تشغيل واحد.
ماذا يحدث إذا تسربت الذاكرة داخل Container محدود بـ 512 MB؟
هذا بالضبط ما حدث لأداة المحاسبة في آخر الشهر، فلنكرره: نشغل الحلقة مليون مرة بالمرجع القوي، ونطبع الذاكرة كل ربع مليون زوج:
docker run --rm --cpus 2 --memory 512m -v "$PWD:/lab" -w /lab swift:6.4.2-noble \
bash -c 'swiftc -O arc.swift -o /tmp/arc; /tmp/arc strong 1000000; echo "exit=$?"; /tmp/arc weak 1000000; echo "exit=$?"'والمخرج سوف يكون كما يلي:
mode=strong start rss=16.0 MB
250000 pairs: alive=500000 rss=310.9 MB
500000 pairs: alive=1000000 rss=525.4 MB
750000 pairs: alive=1500000 rss=524.2 MB
bash: line 1: 33 Killed /tmp/arc strong 1000000
exit=137
mode=weak start rss=16.2 MB
250000 pairs: alive=2 rss=16.2 MB
500000 pairs: alive=2 rss=16.2 MB
750000 pairs: alive=2 rss=16.2 MB
1000000 pairs: alive=2 rss=16.2 MB
mode=weak after 1000000 pairs: alive=0 rss=16.0 MB
exit=0في المخرج أعلاه لاحظ التالي:
- مع المرجع القوي وصلت الذاكرة المقيمة إلى 525 MB عند نصف مليون زوج، ثم ثبتت تقريباً عند 524 MB رغم أن الكائنات الحية زادت إلى مليون ونصف، والسبب أن الـ Container وصل إلى حده، فبدأ النظام ينقل صفحات من الذاكرة إلى التبديل Swap ليبقي العملية حية، ثم لم يبق مكان فقتلها قاتل نفاد الذاكرة OOM Killer.
- رمز الخروج 137 هو 128 + 9، أي أن العملية قتلت بالإشارة
SIGKILL، ولم يطبع البرنامج أي رسالة خطأ لأنه لم يعرف أنه قتل. فإذا رأيت هذا الرمز في Kubernetes مع السببOOMKilledفابحث عن تسرب أو عن حد ذاكرة صغير، وليس عن خطأ في الكود يطبع شيئاً. - مع
weakعالجنا مليون زوج والذاكرة 16.2 MB في كل مرحلة، والعدد الحي 2 هو العميل والطلب في الدورة الحالية فقط.
والدرس لفريق المحاسبة: التسرب في Swift لا يظهر كخطأ، وإنما كذاكرة تصعد ولا تنزل حتى يقتل النظام العملية. ولكي تكتشفه قبل الإنتاج تابع الذاكرة المقيمة في اختبار يعالج بيانات كبيرة، أو استخدم الأدوات التي يشرحها دليل تسرب الذاكرة واستهلاكها على Linux مثل Valgrind وheaptrack، وعلى Mac أداة Memory Graph في Xcode.
الأخطاء Errors في Swift: throws و try و Result
نعود إلى رسالة مطور التطبيق. في Java وPython وC# الطريقة المعتادة للفشل هي الاستثناء Exception، يرمى من أي مكان ويصعد في المكدس حتى يلتقطه أحد، والدالة لا تقول في توقيعها هل ترمي أم لا، إلا في الاستثناءات المفحوصة في Java التي يتجنبها أغلب الناس. وفي Go الخطأ قيمة ترجعها الدالة مع النتيجة، ويجب أن تفحصها بـ if err != nil بعد كل استدعاء. وSwift أخذت شيئاً من الاثنين: الدالة التي قد تفشل تعلن ذلك في توقيعها بـ throws، والمستدعي مجبر أن يكتب try قبل الاستدعاء، فترى في الكود كل نقطة قد يخرج منها التنفيذ، ولكنك لا تكتب if err بعد كل سطر، والخطأ يصعد تلقائياً إلى أقرب do/catch. وفريق اللغة يشرح هذا الاختيار في وثيقة Error Handling Rationale، ومن أهم ما فيها أن الخطأ في Swift ينفذ قريباً من قيمة إرجاع إضافية، وليس كفك المكدس Stack Unwinding في C++، ولذلك فإن رمي الخطأ رخيص ويمكن استخدامه في مسارات عادية مثل «المخزون لا يكفي».
الأخطاء ذات النوع Typed Throws في Swift 6
في مكتبة المخزون عرفنا الأخطاء كـ enum يطبق البروتوكول Error، واستخدمنا ميزة Swift 6 التي تحدد نوع الخطأ في التوقيع، وهي موضحة في المقترح SE-0413 Typed Throws:
enum OrderError: Error, Equatable {
case badQuantity(Int)
case outOfStock(productId: Int, available: Int)
}
struct Inventory {
var stock: [Int: Int] = [1: 10, 2: 0, 3: 4]
// typed throws (Swift 6): the caller knows the only error type
mutating func reserve(_ productId: Int, qty: Int) throws(OrderError) {
guard qty > 0 else { throw .badQuantity(qty) }
let available = stock[productId] ?? 0 // dictionary lookup is Optional
guard available >= qty else { throw .outOfStock(productId: productId, available: available) }
stock[productId] = available - qty
}
}
var inv = Inventory()
for (id, qty) in [(1, 3), (2, 1), (3, 0)] {
do {
try inv.reserve(id, qty: qty)
print("reserved \(qty) of \(id), left \(inv.stock[id]!)")
} catch .outOfStock(let pid, let available) {
print("out of stock: product \(pid), available \(available)")
} catch {
print("other error: \(error)") // error is OrderError here, not any Error
}
}والمخرج سوف يكون كما يلي:
reserved 3 of 1, left 7
out of stock: product 2, available 0
other error: badQuantity(0)في الكود والمخرج أعلاه لاحظ التالي:
mutatingقبل الدالة ضرورية لأنInventoryهيstruct، والدالة التي تغير قيمة من نوع قيمة يجب أن تعلن ذلك، ولذلك لا يمكن أن تستدعيها على متغيرlet.throws(OrderError)تعني أن الدالة لا ترمي إلا هذا النوع، ولذلك استطعنا أن نكتبcatch .outOfStock(let pid, let available)مباشرة، وerrorفي الـcatchالأخير نوعهOrderErrorوليسany Error. وبدون النوع، أيthrowsفقط، يكون الخطأ أي شيء يطبقError.- الحالة
badQuantity(0)طبعت باسمها وقيمتها بدون أي كود، لأن Swift تطبع الـenumبحالته وقيمه المرتبطة. - لا يوجد Stack Trace في الخطأ، فالخطأ في Swift قيمة عادية تحمل ما تضعه فيها فقط، ولذلك ضع فيها ما تحتاجه لفهم المشكلة، مثل رقم المنتج والكمية المتوفرة كما فعلنا.
ونصيحة الفريق في المقترح نفسه أن تستخدم الأخطاء ذات النوع في الكود الداخلي والمكتبات الصغيرة التي تعرف كل حالات فشلها، وأن تبقى على throws العامة في الواجهات العامة التي قد تضيف أخطاء جديدة لاحقاً، لأن إضافة حالة أو نوع خطأ جديد إلى throws(OrderError) تجبر كل من يستدعيك على تعديل كوده.
try? و try! و Result
let quiet = try? inv.reserve(2, qty: 5)
print("try? gave:", quiet as Any)
let r = Result { () throws(OrderError) in try inv.reserve(3, qty: 2) }
switch r {
case .success: print("Result: success, left \(inv.stock[3]!)")
case .failure(let e): print("Result: failure \(e)")
}وعند الترجمة أعطانا المترجم هذا التحذير، ثم المخرج:
errors.swift:32:5: warning: constant 'quiet' inferred to have type '()?', which may be unexpected
32 | let quiet = try? inv.reserve(2, qty: 5)
| |- warning: constant 'quiet' inferred to have type '()?', which may be unexpected
| `- note: add an explicit type annotation to silence this warning
try? gave: nil
Result: success, left 2في المخرج أعلاه لاحظ التالي:
try?تحول أي خطأ إلىnil، فتعطيك اختيارياً من نوع ما ترجعه الدالة، والدالة هنا لا ترجع شيئاً، أيVoid، فصار النوع()?، والمترجم نبهنا أن هذا غريب. والأخطر أن السبب ضاع، فنحن نعرف أن الحجز فشل، ولكن لا نعرف هل السبب المخزون أم الكمية، وسوف نرى في مقال الأخطاء الشائعة كيف أخفتtry?سبب فشل قراءة ملف إعدادات كاملاً.Resultنوع في المكتبة القياسية فيه حالتان.successو.failure، وهو مفيد عندما تريد أن تخزن النتيجة أو تمررها أو تجمع نتائج عدة عمليات قبل أن تقرر، كما في توثيق Result. وفي الكود الجديد معasync/awaitصار استخدامه أقل، لأنthrowsتعمل مع الدوال غير المتزامنة مباشرة.
وبقيت try!، وهي تقول «أنا متأكد أن هذه الدالة لن ترمي»، وإذا رمت توقف البرنامج، وهي ابنة عم علامة التعجب مع الاختياري، وهذا ما نراه الآن.
التوقف Traps: ما لا يمكن التقاطه
في Swift نوعان من الفشل يجب ألا تخلط بينهما. الأول الأخطاء التي تتوقعها وتستطيع أن تتعامل معها، مثل المخزون الذي لا يكفي أو ملف غير موجود، وهذه تمر عبر throws أو Optional. والثاني أخطاء المبرمج Programmer Errors، أي حالات يجب ألا تحدث إذا كان الكود صحيحاً: فتح اختياري فارغ بالقوة، أو رقم خارج حدود المصفوفة، أو تجاوز حد العدد الصحيح، أو try! على دالة رمت. وهذه لا ترمي خطأ يمكن التقاطه، وإنما توقف البرنامج فوراً، وهذا ما يسمى Trap. والسبب أن اللغة ترى أن البرنامج الذي وصل إلى حالة لم يتوقعها مبرمجه لا يمكن الوثوق بما سوف يفعله بعدها، فالتوقف أفضل من الاستمرار بقيمة خاطئة. شغلنا الحالات الأربع:
switch which {
case "unwrap": print(missing!)
case "index": print(empty[0])
case "overflow":
var q = Int.max - 1
q += Int(CommandLine.arguments.count) // runtime value, so the compiler cannot fold it
print(q)
case "trybang": try! inv.reserve(2, qty: 1)
default: print("no trap requested")
}والمخرج مع بناء الإنتاج -O سوف يكون كما يلي:
--- trap: unwrap
*** Signal 4: Backtracing from 0x5615f6840112... done ***
*** Program crashed: Illegal instruction at 0x00005615f6840112 ***
Platform: x86_64 Linux (Ubuntu 24.04.5 LTS)
Thread 0 "errors" crashed:
0 0x00005615f6840112 main + 2914 in errors
exit=132
--- trap: index
*** Program crashed: Illegal instruction at 0x00005644847f6110 ***
exit=132
--- trap: overflow
*** Program crashed: Illegal instruction at 0x0000562e437d4114 ***
exit=132
--- trap: trybang
errors/errors.swift:51: Fatal error: 'try!' expression unexpectedly raised an error: errors.OrderError.outOfStock(productId: 2, available: 0)
*** Program crashed: Illegal instruction at 0x00007f373174cfbf ***
exit=132ونفس الحالات مع بناء التصحيح -Onone:
--- trap: unwrap
main/errors.swift:45: Fatal error: Unexpectedly found nil while unwrapping an Optional value
*** Program crashed: Illegal instruction at 0x00007f179a15f1ef ***
exit=132
--- trap: index
Swift/ContiguousArrayBuffer.swift:695: Fatal error: Index out of range
*** Program crashed: Illegal instruction at 0x00007fa895dca1ef ***
exit=132في المخرج أعلاه لاحظ التالي:
- كل الحالات أنهت البرنامج برمز 132، أي 128 + 4، والإشارة 4 هي
SIGILL، لأن المترجم يضع في مكان الفحص تعليمة غير صالحة تتعمد إيقاف المعالج، وهذا أسرع طريقة لإيقاف البرنامج دون أن يكمل خطوة واحدة. - تجاوز العدد الصحيح أوقف البرنامج حتى في بناء الإنتاج، وهذا يختلف عن C وJava حيث يلتف الرقم بصمت إلى رقم سالب. فالفحص يبقى مع
-O، ولا يحذف إلا مع-Ounchecked، وإذا أردت الالتفاف عمداً فاكتب&+كما رأينا في المقال الأول. - في بناء الإنتاج اختفت رسالة «Unexpectedly found nil» و«Index out of range»، وبقي فقط عنوان التوقف وأنه في
main، بينما ظهرت الرسالة كاملة مع اسم الملف ورقم السطر في بناء التصحيح. والسبب أن المترجم في-Oيحذف نصوص الرسائل من هذه الفحوص ليصغر الكود ويسرعه، ويبقي التوقف نفسه. أماtry!فطبعت رسالتها حتى في الإنتاج لأنها تحمل الخطأ نفسه. - Swift على Linux تطبع Backtrace عند التوقف، وهي ميزة مفعلة في الإصدارات الحديثة، وفي الإنتاج يكفيك سطر الدالة والعنوان مع ملفات الرموز Symbols لتعرف المكان، وقد شرحنا قراءة هذه السجلات في قراءة الـ Stack Trace والتصحيح.
وهذا يجيب على سؤال مطور التطبيق: Swift لم تقرأ ذاكرة خاطئة، وإنما أوقفت التطبيق لأن أحداً كتب ! على قيمة كانت فارغة، والإصلاح ليس في اللغة وإنما في ذلك السطر: if let أو guard let أو قيمة افتراضية بـ ??.

الأنواع غير القابلة للنسخ Noncopyable Types: ملكية على طريقة Rust
رأينا أن Swift تنسخ القيم بحرية وتعتمد على COW وARC لتجعل ذلك رخيصاً، ولكن هناك حالات لا تريد فيها أي نسخة أصلاً: مقبض ملف File Handle يجب أن يغلق مرة واحدة، أو اتصال بقاعدة البيانات يجب ألا يستخدمه مكانان، أو مفتاح تشفير لا تريد أن تبقى منه نسخ في الذاكرة. ولهذه الحالات أضافت اللغة في Swift 5.9 الأنواع غير القابلة للنسخ، وتكتبها بـ ~Copyable بعد اسم النوع، كما في المقترح SE-0390. والقيمة من هذا النوع لها مالك واحد، وإذا مررتها إلى دالة تأخذها بـ consuming انتقلت ملكيتها ولم يعد بإمكانك استخدامها، والمترجم يفحص ذلك كما يفحص Rust الملكية، ويمكن أن تكتب لها deinit حتى لو كانت struct، فيغلق الملف عند نهاية عمر القيمة بالضبط، بدون عداد مراجع وبدون حجز في الـ Heap.
وهذا الاتجاه مستمر، ففي Swift 6.4 أضيفت أنواع مصفوفات تحمل عناصر غير قابلة للنسخ بدون تكلفة COW، وبروتوكول جديد للمرور على العناصر بدون نسخها، بحسب إعلان الإصدار. ولن تحتاج هذه الأدوات في أغلب كود الخدمات والتطبيقات، ولكن من المهم أن تعرف أنها موجودة، لأن Swift تقف في المنتصف: ARC وCOW للكود اليومي، والملكية الصريحة لمن يحتاج أداء وتحكماً مثل C++ وRust، والفكرة كلها موضحة في وثيقة Ownership Manifesto التي كتبها فريق اللغة.
كيف تبدو هذه القرارات في كود المتجر؟ Design
لنطبق ما رأيناه على المكتبة والأداة والخدمة التي سوف نبنيها في المقال الرابع. فالسعر Price والمنتج وسطر الطلب والطلب نفسه كلها بيانات بلا هوية، فهي struct، وتمر بين الدوال وبين المهام المتزامنة بدون أي قلق، لأن كل مستلم يأخذ نسخته. وأخطاء الحجز enum يطبق Error، والحالات المتوقعة مثل «المخزون لا يكفي» تمر عبر throws، وتتحول في طبقة الـ API إلى رد HTTP مناسب مثل 409. أما الأشياء التي لها هوية وحالة مشتركة، مثل مجمع الاتصالات بقاعدة البيانات Connection Pool أو ذاكرة مؤقتة للأسعار يقرأ منها كل الطلبات، فهي class أو actor، ويكون عددها قليلاً ويعيش طول عمر الخدمة، فلا يهمنا ثمن ARC فيها.
وأداة المحاسبة نفسها يصلحها سطر واحد: أن يصبح المرجع من الطلب إلى العميل weak. ولكن الإصلاح الأفضل أن تسأل هل يحتاج الطلب كائن العميل أصلاً، والإجابة أنه يحتاج رقمه واسمه فقط، فيصبح Order قيمة struct فيها customerId وcustomerName، وتختفي الدورة ويختفي معها السؤال. وهذه قاعدة عامة في Swift: إذا وجدت نفسك تفكر في weak وunowned كثيراً في كود بيانات عادي، فاسأل أولاً هل تحتاج هذه الكلاسات أصلاً.
مقارنة مع Java و Python و Go و Rust
| الموضوع | Java | Python | Go | Rust | Swift |
|---|---|---|---|---|---|
| القيمة الافتراضية للأنواع | مراجع | مراجع | قيم | قيم بملكية | قيم، والكلاس مرجع |
| الذاكرة | GC | عد مراجع + GC للدورات | GC | Ownership | ARC بدون GC |
| الدورات المرجعية | يجمعها GC | يجمعها GC | يجمعها GC | نادرة، وWeak | تسرب، والحل weak |
| القيمة الفارغة | null لأي مرجع | None | nil للمؤشرات | Option<T> | T? |
| الأخطاء المتوقعة | استثناءات | استثناءات | قيمة error | Result و? | throws وtry |
| تجاوز العدد الصحيح | يلتف بصمت | لا يحدث | يلتف بصمت | يتوقف في التصحيح فقط | يتوقف دائماً |
ولاحظ أن Python تستخدم عد المراجع أيضاً، ولكنها تضيف جامعاً للدورات يعمل كل فترة، ولذلك لا تتسرب الدورات فيها كما تتسرب في Swift، بينما Swift اختارت ألا يكون هناك أي جامع، وتركت لك مسؤولية weak.
مشاكل واجهناها أثناء التجربة Troubleshooting
- المخرج اختفى عند توقف البرنامج: في التشغيل الأول لتجربة
unownedلم يظهر سطر «weak now: nil» رغم أنه قبل التوقف، والسبب أن المخرج إلى Pipe يحفظ في ذاكرة مؤقتة Buffer ولا يكتب إلا عند امتلائها أو عند الخروج الطبيعي، والتوقف لا يكتب ما بقي. والحل في أدوات التجارب والسكربتات أن تجعل المخرج بلا ذاكرة مؤقتة بـsetvbuf(stdout, nil, _IONBF, 0)، وفي الخدمات أن تكتب السجلات Logs بمكتبة مثل swift-log تكتب كل سطر فوراً. - رسالة التوقف غير موجودة في الإنتاج: ظننا في البداية أن البرنامج يتوقف بلا سبب، حتى قارنا بين
-Oو-Onone. وإذا وصلك توقف من الإنتاج بدون رسالة فابحث في السطر الذي يشير إليه العنوان عن!أو فهرس أو عملية حسابية. - ضجيج القياس: أول تشغيل لتجربة
structوclassأعطى 1.93 ثانية لبناء القيم، أي أبطأ من الكائنات، وهو رقم لا معنى له جاء من جهاز مشغول، ولذلك أعدنا التجربة خمس مرات وقرأنا الوسيط. - وضع اللغة في
swiftc: ترجمنا ملفات التجربة بـswiftcمباشرة، واكتشفنا أنswiftcبدون خيارات يترجم بوضع Swift 5، بينما الحزمة التي فيهاswift-tools-version: 6.xتبنى بوضع Swift 6، فالملف الذي يترجم بالأمر الأول قد يرفضه الثاني. ولذلك اكتب-swift-version 6صراحة عندما تترجم ملفاً منفرداً، وتركناnonisolated(unsafe)على العداد حتى يترجم الملف في الوضعين.
أخطاء شائعة Common Mistakes
- كل نوع
class: عادة من Java تعطيك مراجع مشتركة لا تحتاجها، وتغييرات تظهر في مكان لم تتوقعه، وثمن ARC وحجز الذاكرة لكل كائن. ابدأ بـstruct، وانتقل إلىclassعندما تحتاج هوية مشتركة أوdeinit. - مرجع قوي في الاتجاهين: الأب يمسك الأبناء والابن يمسك الأب، أو كائن يخزن إغلاقاً يستخدم
self. اجعل المرجع إلى الأعلىweak. unownedلتجنب الاختياري: تريحك من?ولكنها تحول أي خطأ في أعمار الكائنات إلى توقف. استخدمها فقط حين يكون العمر مرتبطاً بشكل مضمون.!وtry!لإسكات المترجم: كل واحدة منها توقف محتمل في الإنتاج بلا رسالة في بناء-O.try?في كل مكان: تحول الفشل إلىnilوتضيع السبب، فلا تستخدمها إلا عندما يكون السبب فعلاً غير مهم.- الاعتماد على
deinitلإغلاق الموارد في كود متزامن: صحيح أن ARC يحرر فوراً، ولكن إذا بقي مرجع في مكان لا تتوقعه، مثل إغلاق في مهمة غير منتهية، فلن تعملdeinit. أغلق الاتصالات والملفات بدالة صريحة أو بـdefer.
قائمة تحقق Checklist: الأنواع والذاكرة والأخطاء في مشروعك
| السؤال | إذا كانت الإجابة نعم |
|---|---|
| هل النوع بيانات بلا هوية، مثل سعر أو سطر في سلة؟ | struct |
| هل يحتاج أكثر من مكان أن يرى نفس النسخة ويغيرها؟ | class، أو actor إذا كان الوصول متزامناً |
هل يشير الابن إلى الأب أو يخزن الكائن إغلاقاً يستخدم self؟ | weak أو [weak self] |
| هل القيمة قد لا توجد؟ | T? مع if let أو guard let أو ?? |
| هل العملية قد تفشل لسبب يجب أن يعرفه المستدعي؟ | throws، وthrows(MyError) في الكود الداخلي |
| هل الحالة يجب ألا تحدث إذا كان الكود صحيحاً؟ | precondition أو fatalError برسالة واضحة |
| هل تعالج بيانات كبيرة في حلقة طويلة؟ | تابع الذاكرة المقيمة RSS في اختبار، وحدد ذاكرة الـ Container |
| هل تقيس الأداء؟ | بناء -O، وعدة تشغيلات، والوسيط |
تدريب عملي
- غير
Customerفيarc.swiftإلىstructوحاول أن تبني الدورة نفسها، واقرأ رسالة المترجم، ثم اشرح لماذا لا يمكن أن تتسرب القيم بنفس الطريقة. - اكتب نوعاً
Ledgerمن نوعstructيحمل مرجعاً داخلياً إلى كلاس، وطبق عليه COW بنفسك باستخدامisKnownUniquelyReferenced، ثم قس أول كتابة بعد النسخ. - أضف إلى
OrderErrorحالة جديدةproductDisabled، وابن الكود، ولاحظ أين يجبرك المترجم على التعديل وأين لا يفعل بسبب الـcatchالعام. - ترجم
errors.swiftبالخيار-Ouncheckedوشغل حالةoverflow، ولاحظ ماذا تغير، ولماذا لا تستخدم هذا الخيار في الإنتاج.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- أغلب الأنواع في Swift قيم تنسخ عند الإسناد، ومنها
StringوArrayوDictionary، والنسخ رخيص بفضل COW حتى أول كتابة، كما رأينا 0.008 مللي ثانية للإسناد و199 مللي ثانية لأول كتابة في مصفوفة 80 MB. - الاختياري
T?نوع حقيقي، وInt?حجمه 9 بايتات، والنوع غير الاختياري لا يمكن أن يكون فارغاً، و!طلب صريح بالتوقف إذا كان فارغاً. - ARC يحرر الكائن فوراً عند آخر مرجع بدون Garbage Collector، ولكنه لا يرى الدورات: مئتا ألف زوج من العملاء والطلبات تسربت 236 MB، ومليون زوج قتلت الـ Container برمز 137، ومع
weakبقيت الذاكرة 16 MB. - القيم المتصلة أسرع بكثير من الكائنات المتفرقة: بناء خمسة ملايين قيمة 0.048 ثانية مقابل 1.08 ثانية للكائنات، والجمع 0.014 مقابل 0.23.
- الأخطاء المتوقعة تمر عبر
throwsوtry، ومع Swift 6 تستطيع أن تحدد نوع الخطأ، وtry?تضيع السبب، وResultللتخزين والتمرير. - أخطاء المبرمج توقف البرنامج برمز 132 حتى في الإنتاج، ومنها تجاوز العدد الصحيح، وبناء
-Oيحذف نص الرسالة ويبقي التوقف.
والمقال التالي في هذه المرحلة عن التزامن في Swift: async/await والمهام والممثلون Actors، وكيف يرفض المترجم سباق البيانات قبل أن يعمل الكود. وهذه مقالات تكمل ما رأيناه هنا:
دليلSwift للمطورين: الأدوات والصياغة Syntax على Linuxثبتنا Swift 6.4.2 على Linux وبنينا مكتبة لحساب السعر والضريبة مع أداة سطر أوامر واختبارات، لترى اللغة مقارنة بما تعرفه وتعرف ما يحتاج Mac وما لا يحتاجه.
دليلإدارة الذاكرة: يدوياً أم Garbage Collector أم Ownership؟نقيس بأيدينا في C وGo وPython وRust من يحرر الذاكرة ومتى، وكم يكلف ذلك من الذاكرة والوقت، وما الأخطاء التي تبقى ممكنة في كل طريقة.
دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.
دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.
دليلأمان الذاكرة Memory Safety: لماذا تدفع الحكومات نحو Rust؟ماذا طلبت الحكومات فعلاً بشأن C وC++، ولماذا تسبب أخطاء الذاكرة أغلب الثغرات الخطيرة، ومتى يكون Rust هو الاختيار الصحيح.
دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.
دليلTypeScript للمطورين: أي الأخطاء تكشفها الأنواع Types؟زرعنا تسعة أخطاء في كود سلة مشتريات وفحصناها بالمترجم tsc بثلاثة إعدادات، لنرى ما يكشفه الوضع الصارم وما يمر رغم كل الأنواع ولماذا تحتاج التحقق وقت التشغيل.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجارب بـ Swift 6.4.2 في الـ Image الرسمية
swift:6.4.2-nobleداخل Container محدود بمعالجين و512 MB.