البرمجة

المراجع والاستعارة في رست

المراجع (References) والاستعارة (Borrowing) والشرائح (Slices) في لغة رست Rust: نظرة معمقة

تُعد لغة رست (Rust) من أكثر لغات البرمجة حداثة وأمانًا، حيث تركّز على الأمان في التعامل مع الذاكرة دون التضحية بالأداء. من أبرز المفاهيم التي تميز رست عن غيرها من اللغات، نجد مفاهيم المراجع (References) والاستعارة (Borrowing) والشرائح (Slices). هذه المفاهيم تعمل بشكل مترابط لتقديم نموذج قوي لإدارة الذاكرة يمنع حدوث الأخطاء التقليدية مثل الاستعمال بعد التحرير (Use After Free) أو المراجع المعلقة (Dangling References) أو الظروف المتسارعة (Race Conditions).

سوف نستعرض في هذا المقال كل من هذه المفاهيم بالتفصيل، مع الأمثلة التوضيحية، التحليل الفني، وفهم دقيق لكيفية تفاعلها مع نظام التملك (Ownership System) الخاص بلغة رست.


1. نظام التملك Ownership في لغة رست: الأساس الذي ينبني عليه كل شيء

لكي نفهم المراجع والاستعارة والشرائح، لا بد من الإلمام أولاً بنظام التملك (Ownership). في رست، لكل قيمة في الذاكرة مالك واحد فقط في وقت معين. عند انتقال الملكية (Move)، يتم نقل القيمة من متغير إلى آخر، ولا يمكن استخدام المتغير الأصلي بعدها.

مثال:

rust
fn main() { let s1 = String::from("hello"); let s2 = s1; // السطر هذا ينقل الملكية من s1 إلى s2 // println!("{}", s1); // هذا سيتسبب في خطأ }

لكن كيف يمكننا استخدام قيمة ما دون نقل ملكيتها؟ هنا يأتي دور المراجع (References).


2. المراجع References: الإشارة إلى البيانات دون تملكها

المراجع في رست هي مؤشرات إلى قيم موجودة دون أن تكون مالكة لها. وهي تأتي على نوعين: مراجع غير قابلة للتغيير (Immutable References) ومراجع قابلة للتغيير (Mutable References).

المراجع غير القابلة للتغيير

يمكن إنشاء عدة مراجع غير قابلة للتغيير لنفس القيمة، ولكن لا يمكن أن توجد أي مرجع قابل للتغيير في نفس الوقت.

rust
fn main() { let s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{}, {}", r1, r2); }

المراجع القابلة للتغيير

يمكن فقط مرجع واحد قابل للتغيير في لحظة زمنية واحدة، ولا يمكن جمعه مع مراجع غير قابلة للتغيير.

rust
fn main() { let mut s = String::from("hello"); let r1 = &mut s; // let r2 = &s; // خطأ: لا يمكن الجمع بين mutable وimmutable r1.push_str(" world"); println!("{}", r1); }

3. الاستعارة Borrowing: القرض بدل الملكية

الاستعارة هي الآلية التي تسمح بتمرير المراجع إلى القيم بدلاً من نقل الملكية. وهنا، لا تصبح الدالة المالكة للقيمة، بل تستعيرها فقط.

rust
fn print_length(s: &String) { println!("Length is: {}", s.len()); } fn main() { let s = String::from("Rust"); print_length(&s); // نستعير القيمة println!("Still usable: {}", s); // ما تزال صالحة لأننا لم ننقل الملكية }

ويمكن استعارة القيمة بشكل قابل للتغيير أيضًا:

rust
fn add_world(s: &mut String) { s.push_str(" world"); } fn main() { let mut s = String::from("Hello"); add_world(&mut s); println!("{}", s); }

قواعد الاستعارة

رست تفرض قواعد صارمة عند الاستعارة:

النوع العدد المسموح به في نفس الوقت التفاعل مع الأنواع الأخرى
مراجع غير قابلة للتغيير عدة مراجع لا يسمح بمرجع قابل للتغيير
مرجع قابل للتغيير مرجع واحد فقط لا يسمح بأي مراجع أخرى

هذه القواعد تضمن سلامة البيانات وتمنع السباقات على الذاكرة، مما يجعل رست مناسبة جدًا للبرمجة المتزامنة (Concurrency).


4. الشرائح Slices: النوافذ على البيانات

الشرائح (Slices) هي نوع خاص من المراجع، وتُستخدم لتمثيل جزء من تسلسل ما، مثل المصفوفات أو النصوص. الشرائح لا تملك البيانات، بل تشير فقط إلى جزء منها.

الشرائح النصية String Slices

rust
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; println!("{} {}", hello, world); }

الشرائح في المصفوفات

rust
fn main() { let arr = [1, 2, 3, 4, 5]; let slice = &arr[1..4]; // تشير إلى [2, 3, 4] println!("{:?}", slice); }

أهمية الشرائح

الشرائح مفيدة جدًا في تمرير أجزاء من البيانات دون الحاجة إلى نسخها. وهذا يساهم في كفاءة الأداء وتقليل استهلاك الذاكرة.


5. مقارنة بين المرجع والاستعارة والشرائح

الخاصية المرجع (Reference) الاستعارة (Borrowing) الشريحة (Slice)
النوع مشير إلى قيمة تمرير مرجع للدالة مرجع إلى جزء من تسلسل
امتلاك القيمة لا لا لا
القابلية للتغيير نعم/لا نعم/لا لا (عادة غير قابلة للتغيير)
الغرض الإشارة إلى بيانات استخدام القيم دون نقل ملكيتها تمثيل جزء من مصفوفة أو سلسلة
المدى يعتمد على السياق مؤقت (خلال الاستعارة فقط) يعتمد على مدة المرجع إلى السلسلة

6. دورة الحياة (Lifetimes): ركيزة الأمان في التعامل مع المراجع

نظرًا لأن رست لا تستخدم جامع نفايات (Garbage Collector)، فإن إدارة دورة حياة المراجع أمر حاسم. تقوم رست بتحليل “مدة صلاحية” المراجع تلقائيًا، لكنها تتيح أيضًا للمبرمج تحديدها عند الحاجة باستخدام رموز lifetimes.

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }

تحديد دورة الحياة يساعد رست على التأكد من أن المرجع لن يكون معلقًا (Dangling). يتم ذلك غالبًا عندما تكون هناك دوال تُرجع مراجع إلى البيانات.


7. حالات شائعة وأخطاء متكررة

المراجع المعلقة Dangling References

rust
fn dangle() -> &String { let s = String::from("hello"); &s // خطأ: s سيتم تحريره قبل إرجاع المرجع }

تجاوز مدة الحياة

عندما يكون المرجع إلى قيمة خرجت من النطاق، يحدث انتهاك لذاكرة، لكن رست تمنع ذلك على مستوى الترجمة.

استعارة مزدوجة متضاربة

rust
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &mut s; // خطأ: لا يمكن الجمع println!("{}, {}", r1, r2); }

8. الأداء والكفاءة: لماذا هذا النموذج أفضل؟

بفضل نموذج التملك والاستعارة، يمكن للكمبايلر في رست أن يضمن في وقت الترجمة:

  • عدم وجود تسرب في الذاكرة

  • عدم وجود مراجع غير صالحة

  • عدم وجود تنافس بين الخيوط (Race Conditions)

  • كفاءة عالية بدون استخدام جامع نفايات

هذه الضمانات تجعل رست مناسبة تمامًا لتطبيقات الأنظمة، البرمجة المضمنة (Embedded Programming)، تطوير محركات الألعاب، المتصفحات، وحتى البنية التحتية للشبكات.


9. الجدول التوضيحي للمفاهيم الأساسية

المفهوم التعريف يملك الذاكرة يمكن تغييره مثال الاستخدام
Reference مؤشر إلى قيمة موجودة لا نعم/لا (حسب النوع) &x أو &mut x
Borrowing تمرير مرجع دون ملكية لا نعم/لا fn f(&x)
Slice مرجع إلى جزء من تسلسل لا لا &arr[1..3]
Ownership نقل ملكية القيمة نعم نعم let x = y;
Lifetime مدة صلاحية المرجع لا لا 'a

10. التطبيقات الواقعية في البرمجة باستخدام رست

  • معالجة الملفات الكبيرة: يمكن قراءة أجزاء من الملف إلى شرائح دون تحميله بالكامل إلى الذاكرة.

  • برمجة الخيوط Threads: من خلال الاستعارة بدلاً من النقل، يمكن تأمين سلامة البيانات دون الحاجة إلى قفل Mutex دائم.

  • واجهات برمجة التطبيقات APIs: تمرير المراجع يقلل من النسخ المكلف، ويزيد الكفاءة.

  • محركات الألعاب: تستخدم الشرائح بكثرة لتحسين الأداء في التعامل مع المصفوفات الكبيرة.


المصادر

  1. The Rust Programming Language (كتاب Rust الرسمي): https://doc.rust-lang.org/book/

  2. Rust Reference Manual: https://doc.rust-lang.org/reference/