> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# الأنواع والذاكرة ARC والأخطاء Errors في Swift
- URL: https://arabroot.io/articles/الانواع-والذاكرة-arc-والاخطاء-في-swift/
- Published: 2026-10-31T07:32:32.000Z
- Updated: 2026-10-31T11:11:04.000Z
- Description: قسنا النسخ عند الكتابة وتسرب الذاكرة بالدورة المرجعية داخل Container محدود، ورأينا كيف تفشل Swift بـ throws والاختياري والتوقف، لتعرف ما يختلف فيها عن Java وPython وGo.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

في [المقال الأول من هذه المرحلة](https://arabroot.io/articles/swift-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B5%D9%8A%D8%A7%D8%BA%D8%A9-%D8%B9%D9%84%D9%89-linux/) نقلنا حساب السعر والضريبة في متجر التمور والقهوة إلى مكتبة 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](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%8A%D8%AF%D9%88%D9%8A%D8%A7-%D8%A7%D9%85-garbage-collector-%D8%A7%D9%85-ownership/)، وطرق اللغات في التعامل مع الأخطاء في [كيف تتعامل لغات البرمجة مع الأخطاء](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)، فلن نكرر النظرية هنا، وإنما نرى كيف تطبقها 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 من القيم والاستدعاءات. وقد شرحنا هذه التقسيمات بين اللغات في مقال [أنماط البرمجة وأنظمة الأنواع](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، والسؤال المهم هنا ليس هل الأنواع ثابتة، فهذا تعرفه من Java، وإنما ماذا يعني النوع في Swift عندما تنسخه أو تمرره إلى دالة.

### أنواع القيم Value Types وأنواع المراجع Reference Types

في Java كل كائن مرجع، وعندما تكتب `b = a` فأنت تنسخ عنواناً، ويشير الاثنان إلى نفس الكائن. وفي Python كل شيء كذلك تقريباً. أما في Swift فهناك عائلتان: أنواع القيم، وهي `struct` و`enum` والـ Tuple، وتنسخ كاملة عند الإسناد وعند التمرير إلى دالة، وأنواع المراجع، وهي `class` والـ Actor والإغلاقات Closures، ويشترك فيها كل من يحمل المرجع. والمفاجأة لمن يأتي من Java أن أنواع المكتبة القياسية الأساسية كلها قيم: `Int` و`String` و`Array` و`Dictionary` و`Set`، ولذلك فإن مصفوفة تمررها إلى دالة لا يمكن أن تتغير من داخل الدالة إلا إذا قلت ذلك صراحة بـ `inout`. والمثال التالي يبين الفرق:

```swift
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:

```bash
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'
```

والمخرج سوف يكون كما يلي:

```text
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 حيث a وb نسختان مستقلتان من CartLine لكل منهما qty، وبعد b.qty = 99 يبقى a.qty يساوي 2، وفي اليسار class حيث r1 وr2 مرجعان يشيران إلى نفس الكائن في الـ Heap ومعه عداد المراجع 2، وبعد r2.qty = 99 يصبح r1.qty أيضاً 99 والمقارنة r1 === r2 صحيحة، وتحتها قاعدة: let مع struct تثبت القيمة كلها ومع class تثبت المرجع فقط](https://arabroot.io/content/images/2026/10/swift-types-memory-01-value-reference.webp)

القيمة تنسخ، والمرجع يشترك فيه الجميع

وقد يتساءل البعض: إذا كانت `struct` تنسخ عند كل إسناد، أليس هذا بطيئاً مع مصفوفة كبيرة؟ والإجابة في الفقرة التالية.

### النسخ عند الكتابة Copy-on-Write: لماذا لا يبطئ النسخ؟

المصفوفة في Swift قيمة من الخارج، ولكنها من الداخل تحمل مرجعاً إلى مخزن Buffer في الـ Heap فيه العناصر. وعندما تنسخ المصفوفة فإن Swift تنسخ المرجع فقط وتزيد عداده، فيشترك المتغيران في نفس المخزن، وعندما يحاول أحدهما أن يكتب تسأل اللغة: هل أنا المالك الوحيد لهذا المخزن؟ فإذا كان الجواب لا نسخت المخزن كاملاً ثم كتبت في النسخة، وإذا كان نعم كتبت مباشرة. وهذا ما يسمى النسخ عند الكتابة Copy-on-Write، واختصاراً COW. ولنقس ذلك بمصفوفة فيها عشرة ملايين رقم:

```swift
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))
```

والمخرج سوف يكون كما يلي:

```text
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](https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes?ref=arabroot.io).

![ثلاث مراحل لمصفوفة بعشرة ملايين رقم: الإسناد 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

وفي نفس الملف سألنا المترجم عن حجم بعض الأنواع:

```text
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](https://developer.apple.com/documentation/swift/optional?ref=arabroot.io). ولهذا السبب لا يمكن أن تصل قيمة فارغة إلى متغير من نوع `Int` أبداً، فالنوع `Int` لا يملك حالة فارغة أصلاً، وكل مكان قد تغيب فيه القيمة يجب أن يقول ذلك في نوعه. وفي Java يمكن أن يكون أي مرجع `null`، والمترجم لا يعرف أيها قد يكون، ولذلك يظهر `NullPointerException` وقت التشغيل، وفي TypeScript حلت المشكلة بالخيار `strictNullChecks` كما رأينا في مقال [TypeScript للمطورين](https://arabroot.io/articles/typescript-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%85%D8%A7-%D8%A7%D9%84%D8%B0%D9%8A-%D8%AA%D9%83%D8%B4%D9%81%D9%87-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9/)، وفي Swift هي جزء من اللغة منذ اليوم الأول.

ولكي تستخدم القيمة داخل الاختياري عليك أن تفتحه Unwrap، ولذلك أكثر من طريقة:

```swift
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`» فأمامك طريقتان:

```swift
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](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/?ref=arabroot.io) في كتاب اللغة. وفي 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](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/?ref=arabroot.io) في كتاب اللغة.

ولهذا الاختيار نتيجتان تهمانك كمطور سيرفر. الأولى أن التحرير حتمي Deterministic، فلا يوجد توقف مفاجئ لجمع القمامة، والذاكرة تعود فور انتهاء آخر مرجع، وهذا ما ذكره فريق Apple عندما [نقل خدمة Password Monitoring من Java إلى Swift](https://www.swift.org/blog/swift-at-apple-migrating-the-password-monitoring-service-from-java/?ref=arabroot.io)، حيث قال إن الإدارة الحتمية للذاكرة جعلت الخدمة تحتاج مئات الميجابايتات لكل نسخة بدل عشرات الجيجابايتات. والثانية أن ARC لا يعرف الدورات، فإذا أشار كائنان كل منهما إلى الآخر فلن يصل أي عداد إلى صفر، وسوف يبقيان في الذاكرة إلى الأبد، وهذا بالضبط ما حدث في أداة المحاسبة.

### كيف يعمل ARC من الداخل: retain و release و deinit

لنرى أولاً متى يحرر الكائن. في ملف `arc.swift` عداد عام `alive` يزيد في دالة البناء وينقص في `deinit`:

```swift
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)")
```

والمخرج سوف يكون كما يلي:

```text
inside scope alive=1
after scope alive=0
```

الكائن حرر في نفس اللحظة التي انتهى فيها النطاق `do`، وليس «في وقت ما لاحقاً». ولاحظ `nonisolated(unsafe)` قبل المتغير العام، وهذه من قواعد Swift 6: المتغير العام القابل للتغيير يمكن أن يصل إليه أكثر من خيط Thread، ولذلك يعزله المترجم على الممثل الرئيسي ويرفض أن تغيره دالة غير معزولة مثل `init` و`deinit`، إلا إذا قلت صراحة إنك تتحمل المسؤولية، وهذا موضوع المقال الثالث عن التزامن. ولأن هذه التجربة تعمل في خيط واحد فالكلمة هنا مقبولة، ولكن لا تكتبها في كود حقيقي دون أن تفكر. وجربنا ملفاً صغيراً بنفس الفكرة بدون الكلمة بوضع Swift 6، فكان المخرج:

```text
/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

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

```swift
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. نشغل الحلقة مئتي ألف مرة بالطريقتين:

```bash
swiftc -O arc.swift -o /tmp/arc
/tmp/arc strong
/tmp/arc weak
```

والمخرج سوف يكون كما يلي:

```text
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 تبقى حتى تنتهي العملية.

![في اليمين دورة مرجعية: كائن Customer يشير بسهم قوي إلى مصفوفة orders وفيها Order، وOrder يشير بسهم قوي إلى نفس Customer، وعداد كل منهما 1 بعد خروج المتغيرات من النطاق فلا يحرر أي منهما، وفي اليسار نفس الشكل مع سهم متقطع weak من Order إلى Customer فيصل عداد العميل إلى صفر ويحرر الاثنان، وفي الأسفل أعمدة الذاكرة بعد 200 ألف زوج: 251.9 MB مع المرجع القوي و16.0 MB مع weak، وبعد مليون زوج مع المرجع القوي قتل الـ Container عند حد 512 MB](https://arabroot.io/content/images/2026/10/swift-types-memory-03-retain-cycle.webp)

مرجع قوي في الاتجاهين يعني أن لا أحد يصل إلى صفر

### المرجع الضعيف weak والمرجع unowned: الفرق وثمنه

في Swift طريقتان لتمنع الدورة من البداية: `weak` و`unowned`. فالمرجع `weak` اختياري دائماً، ويصبح `nil` تلقائياً عندما يحرر الكائن، ولذلك هو آمن ولكنه يجبرك أن تتعامل مع غياب القيمة. والمرجع `unowned` ليس اختيارياً، فهو وعد منك بأن الكائن سوف يعيش أطول من المرجع، مثل الفاتورة التي لا توجد بدون عميلها. ولنرى ماذا يحدث إذا لم تف بالوعد:

```swift
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
```

والمخرج سوف يكون كما يلي:

```text
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](https://arabroot.io/articles/%D8%A7%D9%85%D8%A7%D9%86-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-memory-safety-%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%AF%D9%81%D8%B9-%D8%A7%D9%84%D8%AD%D9%83%D9%88%D9%85%D8%A7%D8%AA-%D9%86%D8%AD%D9%88-rust/)، وSwift من اللغات التي تذكرها تلك التوصيات كلغة آمنة للذاكرة.

والدورات لا تأتي من الكلاسات فقط، فالإغلاق Closure نوع مرجعي، وإذا خزنت إغلاقاً في خاصية واستخدم الإغلاق `self` فقد صنعت دورة بين الكائن والإغلاق، وهذا أشهر مصدر للتسرب في تطبيقات iOS، وسوف نرى مثاله الحي والإصلاح بـ `[weak self]` في مقال الأخطاء الشائعة.

### كم يكلف ARC؟ struct مقابل class

بعد أن رأينا كيف يعمل ARC بقي سؤال الأداء: ما ثمن الكائنات المرجعية مقارنة بالقيم؟ فكتبنا تجربة تبني خمسة ملايين سعر كـ `struct` مرة وكـ `class` مرة، ثم تجمعها، ثم تحرر الكائنات:

```swift
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()
```

وشغلناها خمس مرات، لأن أول تشغيل كان فيه ضجيج واضح من الجهاز المضيف:

```text
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 |

![أعمدة أفقية للوسيط من خمسة تشغيلات لخمسة ملايين سعر: البناء 0.048 ثانية مع struct و1.084 ثانية مع class، والجمع 0.014 ثانية مع struct و0.231 ثانية مع class، وتحرير الكائنات 0.577 ثانية مع class، وبجانبها شرح أن struct عناصر متصلة 8 بايتات لكل سعر بينما class مؤشرات إلى خمسة ملايين كائن متفرق في الـ Heap لكل منها رأس وعداد مراجع](https://arabroot.io/content/images/2026/10/swift-types-memory-04-struct-class-cost.webp)

نفس الأرقام ونفس الجمع: القيم المتصلة أسرع بعشرين ضعفاً تقريباً

والسبب ليس ARC وحده، وإنما ثلاثة أشياء معاً: كل كائن يحتاج حجزاً منفصلاً من الـ Heap بدل مكان في مخزن واحد متصل، وكل كائن له رأس فيه عداد المراجع، والمرور على المصفوفة يقفز بين عناوين متفرقة في الذاكرة بدل أن يقرأ بايتات متجاورة تستفيد منها ذاكرة المعالج المؤقتة Cache. وهذا ما توصي به [إرشادات Swift على السيرفر عن الحجز](https://www.swift.org/documentation/server/guides/allocations.html?ref=arabroot.io): قلل الحجوزات في المسارات الساخنة، والطريقة الأسهل لذلك أن تكون أنواعك قيماً. ولاحظ أن الفرق بين التشغيلات كبير، فالجمع مع `class` أخذ مرة 0.20 ومرة 0.44 ثانية، وهذا ضجيج جهاز تطوير يعمل عليه غيره، ولذلك نقرأ الوسيط ولا نعتمد على تشغيل واحد.

### ماذا يحدث إذا تسربت الذاكرة داخل Container محدود بـ 512 MB؟

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

```bash
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=$?"'
```

والمخرج سوف يكون كما يلي:

```text
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 لا يظهر كخطأ، وإنما كذاكرة تصعد ولا تنزل حتى يقتل النظام العملية. ولكي تكتشفه قبل الإنتاج تابع الذاكرة المقيمة في اختبار يعالج بيانات كبيرة، أو استخدم الأدوات التي يشرحها دليل [تسرب الذاكرة واستهلاكها](https://www.swift.org/documentation/server/guides/memory-leaks-and-usage.html?ref=arabroot.io) على 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](https://github.com/swiftlang/swift/blob/main/docs/ErrorHandlingRationale.md?ref=arabroot.io)، ومن أهم ما فيها أن الخطأ في Swift ينفذ قريباً من قيمة إرجاع إضافية، وليس كفك المكدس Stack Unwinding في C++، ولذلك فإن رمي الخطأ رخيص ويمكن استخدامه في مسارات عادية مثل «المخزون لا يكفي».

### الأخطاء ذات النوع Typed Throws في Swift 6

في مكتبة المخزون عرفنا الأخطاء كـ `enum` يطبق البروتوكول `Error`، واستخدمنا ميزة Swift 6 التي تحدد نوع الخطأ في التوقيع، وهي موضحة في المقترح [SE-0413 Typed Throws](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md?ref=arabroot.io):

```swift
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
    }
}
```

والمخرج سوف يكون كما يلي:

```text
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

```swift
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)")
}
```

وعند الترجمة أعطانا المترجم هذا التحذير، ثم المخرج:

```text
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](https://developer.apple.com/documentation/swift/result?ref=arabroot.io). وفي الكود الجديد مع `async/await` صار استخدامه أقل، لأن `throws` تعمل مع الدوال غير المتزامنة مباشرة.

وبقيت `try!`، وهي تقول «أنا متأكد أن هذه الدالة لن ترمي»، وإذا رمت توقف البرنامج، وهي ابنة عم علامة التعجب مع الاختياري، وهذا ما نراه الآن.

### التوقف Traps: ما لا يمكن التقاطه

في Swift نوعان من الفشل يجب ألا تخلط بينهما. الأول الأخطاء التي تتوقعها وتستطيع أن تتعامل معها، مثل المخزون الذي لا يكفي أو ملف غير موجود، وهذه تمر عبر `throws` أو `Optional`. والثاني أخطاء المبرمج Programmer Errors، أي حالات يجب ألا تحدث إذا كان الكود صحيحاً: فتح اختياري فارغ بالقوة، أو رقم خارج حدود المصفوفة، أو تجاوز حد العدد الصحيح، أو `try!` على دالة رمت. وهذه لا ترمي خطأ يمكن التقاطه، وإنما توقف البرنامج فوراً، وهذا ما يسمى Trap. والسبب أن اللغة ترى أن البرنامج الذي وصل إلى حالة لم يتوقعها مبرمجه لا يمكن الوثوق بما سوف يفعله بعدها، فالتوقف أفضل من الاستمرار بقيمة خاطئة. شغلنا الحالات الأربع:

```swift
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` سوف يكون كما يلي:

```text
--- 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`:

```text
--- 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 والتصحيح](https://arabroot.io/articles/%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-%D8%B1%D8%B3%D8%A7%D9%84%D8%A9-%D8%A7%D9%84%D8%AE%D8%B7%D8%A7-stack-trace-%D9%88%D8%A7%D9%84%D8%AA%D8%B5%D8%AD%D9%8A%D8%AD-debugging/).

وهذا يجيب على سؤال مطور التطبيق: Swift لم تقرأ ذاكرة خاطئة، وإنما أوقفت التطبيق لأن أحداً كتب `!` على قيمة كانت فارغة، والإصلاح ليس في اللغة وإنما في ذلك السطر: `if let` أو `guard let` أو قيمة افتراضية بـ `??`.

![خريطة لطرق الفشل في Swift: في اليمين الأخطاء المتوقعة وفيها Optional لقيمة قد لا توجد مثل قاموس بمفتاح غير موجود، وthrows مع try وdo/catch لعملية قد تفشل مع سبب، وthrows(OrderError) عندما تعرف كل الأسباب، وResult لتخزين النتيجة أو تمريرها، وفي اليسار أخطاء المبرمج التي توقف البرنامج: القوة ! على nil، والفهرس خارج الحدود، وتجاوز العدد الصحيح، وtry! على دالة رمت، وunowned بعد التحرير، مع رمز الخروج 132 أو 134، وملاحظة أن -O يحذف نص الرسالة](https://arabroot.io/content/images/2026/10/swift-types-memory-05-failure-map.webp)

ما تتوقعه يمر عبر النوع، وما لا يجب أن يحدث يوقف البرنامج

## الأنواع غير القابلة للنسخ Noncopyable Types: ملكية على طريقة Rust

رأينا أن Swift تنسخ القيم بحرية وتعتمد على COW وARC لتجعل ذلك رخيصاً، ولكن هناك حالات لا تريد فيها أي نسخة أصلاً: مقبض ملف File Handle يجب أن يغلق مرة واحدة، أو اتصال بقاعدة البيانات يجب ألا يستخدمه مكانان، أو مفتاح تشفير لا تريد أن تبقى منه نسخ في الذاكرة. ولهذه الحالات أضافت اللغة في Swift 5.9 الأنواع غير القابلة للنسخ، وتكتبها بـ `~Copyable` بعد اسم النوع، كما في المقترح [SE-0390](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md?ref=arabroot.io). والقيمة من هذا النوع لها مالك واحد، وإذا مررتها إلى دالة تأخذها بـ `consuming` انتقلت ملكيتها ولم يعد بإمكانك استخدامها، والمترجم يفحص ذلك كما يفحص Rust الملكية، ويمكن أن تكتب لها `deinit` حتى لو كانت `struct`، فيغلق الملف عند نهاية عمر القيمة بالضبط، بدون عداد مراجع وبدون حجز في الـ Heap.

وهذا الاتجاه مستمر، ففي Swift 6.4 أضيفت أنواع مصفوفات تحمل عناصر غير قابلة للنسخ بدون تكلفة COW، وبروتوكول جديد للمرور على العناصر بدون نسخها، بحسب [إعلان الإصدار](https://www.swift.org/blog/swift-6.4-released/?ref=arabroot.io). ولن تحتاج هذه الأدوات في أغلب كود الخدمات والتطبيقات، ولكن من المهم أن تعرف أنها موجودة، لأن Swift تقف في المنتصف: ARC وCOW للكود اليومي، والملكية الصريحة لمن يحتاج أداء وتحكماً مثل C++ وRust، والفكرة كلها موضحة في وثيقة [Ownership Manifesto](https://github.com/swiftlang/swift/blob/main/docs/OwnershipManifesto.md?ref=arabroot.io) التي كتبها فريق اللغة.

## كيف تبدو هذه القرارات في كود المتجر؟ 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، وكيف يرفض المترجم سباق البيانات قبل أن يعمل الكود. وهذه مقالات تكمل ما رأيناه هنا:

[![](https://arabroot.io/content/images/2026/10/swift-for-devs-cover-cover.webp)دليلSwift للمطورين: الأدوات والصياغة Syntax على Linuxثبتنا Swift 6.4.2 على Linux وبنينا مكتبة لحساب السعر والضريبة مع أداة سطر أوامر واختبارات، لترى اللغة مقارنة بما تعرفه وتعرف ما يحتاج Mac وما لا يحتاجه.](https://arabroot.io/articles/swift-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B5%D9%8A%D8%A7%D8%BA%D8%A9-%D8%B9%D9%84%D9%89-linux/)[![](https://arabroot.io/content/images/2026/10/memory-management-cover-cover.webp)دليلإدارة الذاكرة: يدوياً أم Garbage Collector أم Ownership؟نقيس بأيدينا في C وGo وPython وRust من يحرر الذاكرة ومتى، وكم يكلف ذلك من الذاكرة والوقت، وما الأخطاء التي تبقى ممكنة في كل طريقة.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%8A%D8%AF%D9%88%D9%8A%D8%A7-%D8%A7%D9%85-garbage-collector-%D8%A7%D9%85-ownership/)[![](https://arabroot.io/content/images/2026/10/error-handling-cover-cover.webp)دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)[![](https://arabroot.io/content/images/2026/10/paradigms-types-cover-cover.webp)دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/memory-safety-rust-cover-cover.webp)دليلأمان الذاكرة Memory Safety: لماذا تدفع الحكومات نحو Rust؟ماذا طلبت الحكومات فعلاً بشأن C وC++، ولماذا تسبب أخطاء الذاكرة أغلب الثغرات الخطيرة، ومتى يكون Rust هو الاختيار الصحيح.](https://arabroot.io/articles/%D8%A7%D9%85%D8%A7%D9%86-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-memory-safety-%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%AF%D9%81%D8%B9-%D8%A7%D9%84%D8%AD%D9%83%D9%88%D9%85%D8%A7%D8%AA-%D9%86%D8%AD%D9%88-rust/)[![](https://arabroot.io/content/images/2026/10/debugging-basics-cover-cover.webp)دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.](https://arabroot.io/articles/%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-%D8%B1%D8%B3%D8%A7%D9%84%D8%A9-%D8%A7%D9%84%D8%AE%D8%B7%D8%A7-stack-trace-%D9%88%D8%A7%D9%84%D8%AA%D8%B5%D8%AD%D9%8A%D8%AD-debugging/)[![](https://arabroot.io/content/images/2026/10/typescript-for-devs-cover-cover.webp)دليلTypeScript للمطورين: أي الأخطاء تكشفها الأنواع Types؟زرعنا تسعة أخطاء في كود سلة مشتريات وفحصناها بالمترجم tsc بثلاثة إعدادات، لنرى ما يكشفه الوضع الصارم وما يمر رغم كل الأنواع ولماذا تحتاج التحقق وقت التشغيل.](https://arabroot.io/articles/typescript-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%85%D8%A7-%D8%A7%D9%84%D8%B0%D9%8A-%D8%AA%D9%83%D8%B4%D9%81%D9%87-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9/)

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

- أكتوبر 2026: كتابة المقال، وتنفيذ التجارب بـ Swift 6.4.2 في الـ Image الرسمية `swift:6.4.2-noble` داخل Container محدود بمعالجين و512 MB.