البرمجة

المؤشر الذكي Rc في رست

المؤشر Rc الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust

تعتبر لغة رست (Rust) من اللغات البرمجية الحديثة التي نالت شهرة واسعة بين مطوري البرمجيات نظرًا لتركيزها القوي على السلامة في إدارة الذاكرة، والأداء العالي، مع القدرة على التحكم الكامل بالمؤشرات والموارد. من أهم التحديات التي تواجه مبرمجي لغات النظام هي كيفية إدارة الذاكرة بشكل آمن وفعّال دون التسبب بتسريبات أو أخطاء في الوصول إلى البيانات. لتلبية هذه الحاجة، تقدم رست أدوات متعددة لإدارة الملكية (ownership) والاقتراض (borrowing) بناءً على قواعد صارمة تساعد في ضمان سلامة الذاكرة وقت الترجمة، دون الحاجة لجمع قمامة Garbage Collector تقليدي. من بين هذه الأدوات التي تقدمها رست للمطورين هو المؤشر الذكي Rc.

ما هو المؤشر الذكي Rc في رست؟

المؤشر الذكي Rc هو اختصار لـ “Reference Counted”، أي “مُعَدّ العد المرجعي”. هو نوع خاص من المؤشرات الذكية يُستخدم لإدارة ملكية البيانات بطريقة تسمح لعدة أجزاء من البرنامج بمشاركة الوصول إلى نفس القيمة (البيانات) في ذاكرة heap مع الحفاظ على تتبع عدد المراجع (references) التي تشير إلى تلك البيانات.

في النظام العادي للملكية في رست، هناك قاعدة واضحة تمنع وجود أكثر من مالك واحد mutable (قابل للتغيير) للبيانات في الوقت نفسه، أو وجود عدة مالكين ولكن فقط للبيانات غير القابلة للتغيير (immutable). هذا لضمان سلامة البيانات ومنع حالات السباق في الذاكرة.

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

خصائص Rc

  • عد المرجع (Reference Counting): يقوم Rc بتتبع عدد المؤشرات التي تشير إلى نفس البيانات. كلما أنشأت نسخة جديدة من Rc، يرتفع العد، وعند انتهاء أي مؤشر منها، ينخفض العد.

  • تخزين على الـ heap: تقوم Rc بتخزين البيانات في الذاكرة الديناميكية (heap)، وليس على المكدس (stack).

  • مشاركة ملكية البيانات: يسمح Rc لمجموعة من المؤشرات بمشاركة ملكية نفس البيانات.

  • عدم قابلية التغيير (Immutability): لا يسمح Rc بالتغيير المباشر على البيانات بشكل افتراضي، حيث تكون البيانات التي يتم مشاركتها عبر Rc غير قابلة للتغيير، ما لم يتم استخدام أدوات إضافية مثل RefCell لتوفير إمكانية تعديل بيانات داخل Rc.

  • ليس آمنًا عبر الخيوط (Not Thread-Safe): Rc مصمم للاستخدام داخل خيط تنفيذ واحد (single-threaded)، وإذا كانت هناك حاجة لمشاركة البيانات بين عدة خيوط، يتم استخدام Arc (Atomic Reference Counted).

آلية عمل Rc وعداد المراجع

في كل مرة يتم إنشاء مؤشر Rc جديد يشير إلى نفس البيانات، فإن عداد المراجع (reference count) يزيد بمقدار واحد. وهذا العد هو قيمة عددية مخزنة داخل هيكل البيانات الخاص بالمؤشر الذكي Rc. عند استدعاء دالة clone() على Rc، يتم إنشاء نسخة جديدة من المؤشر ترفع العد. وعند انتهاء أي مؤشر Rc من العمل (عندما يتم إسقاطه أو يخرج من النطاق scope)، ينخفض العد. وعندما يصل عداد المراجع إلى صفر، يتم تلقائيًا تحرير الذاكرة المخصصة للبيانات.

هذه الطريقة تتيح إدارة ذاكرة آمنة وفعالة بدون تدخل المستخدم في عمليات الحذف، ما يقلل من احتمالية حدوث أخطاء مثل “استخدام بعد الحذف” أو “تسريبات الذاكرة”.

كيف يُحتفظ بعداد المراجع داخليًا؟

يحتوي Rc على حقل داخلي يُدير العد المرجعي باستخدام عداد بيانات (integer counter) يتم تحديثه بشكل تلقائي داخل التنفيذ الداخلي للمؤشر. هذا العداد مخزن في مكان يُشترك به بين كل نسخ Rc التي تشير إلى نفس البيانات. يُدار هذا العداد بواسطة آليات خاصة في لغة رست تضمن التزامن الداخلي وآلية الإضافة والطرح الآمن للعداد في سياق الخيط الواحد.

مثال توضيحي

rust
use std::rc::Rc; fn main() { let rc_a = Rc::new(String::from("مرحبًا بالعالم")); println!("عدد المراجع بعد الإنشاء: {}", Rc::strong_count(&rc_a)); // 1 let rc_b = Rc::clone(&rc_a); println!("عدد المراجع بعد النسخ: {}", Rc::strong_count(&rc_a)); // 2 { let rc_c = Rc::clone(&rc_a); println!("عدد المراجع داخل النطاق: {}", Rc::strong_count(&rc_a)); // 3 } println!("عدد المراجع بعد انتهاء النطاق: {}", Rc::strong_count(&rc_a)); // 2 }

في المثال أعلاه، نلاحظ كيف أن عداد المراجع يزداد وينقص مع نسخ المؤشرات وانتهاء نطاقها.

حالات الاستخدام المناسبة لـ Rc

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

  • الهياكل البيانية (Data Structures) المعقدة: مثل الأشجار أو الرسوم البيانية حيث تحتاج العقد إلى امتلاك مراجع متعددة لعناصر أخرى.

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

مثال على هياكل البيانات المعقدة

يمكن استخدام Rc لبناء هياكل بيانات مثل القوائم المرتبطة (linked lists) التي تسمح بمشاركة العقد، أو الرسوم البيانية التي لها حلقات (cycles). لكن يجب الحذر مع الحلقات لأنه قد يؤدي إلى تسريبات ذاكرة بسبب عدم تحرير البيانات عندما يكون هناك حلقات بين Rc.

لهذا الغرض، تقدم رست نوعًا ذكيًا آخر يدعى Weak يمكن استخدامه لكسر الحلقة المرجعية بين Rc.

الفرق بين Rc و Arc

رغم تشابه Rc و Arc في آلية عد المرجع، إلا أن الفرق الرئيسي بينهما هو أن:

  • Rc مخصص للاستخدام داخل خيط تنفيذ واحد، حيث لا توجد مشاكل سباق في تعديل العداد المرجعي.

  • Arc هو نسخة مؤمنة عبر الخيوط (thread-safe) باستخدام آليات التزامن الذري atomic synchronization، مما يسمح بمشاركة البيانات بين خيوط متعددة في نفس الوقت.

لذلك، في بيئات متعددة الخيوط (multithreading) يجب استخدام Arc بدلاً من Rc.

Rc مع RefCell لتعديل البيانات المشتركة

نظرًا لأن Rc يشارك ملكية البيانات مع جعلها غير قابلة للتغيير بشكل افتراضي، فإن الحاجة للتعديل المشترك تظهر في بعض الحالات. لحل هذه المشكلة توفر رست أداة أخرى تدعى RefCell التي تسمح بالتعديل الداخلي للبيانات داخل Rc عبر تقنية تسمى “التغيير الداخلي” (Interior Mutability).

التركيب التالي شائع في رست:

rust
use std::rc::Rc; use std::cell::RefCell; fn main() { let data = Rc::new(RefCell::new(5)); { let mut value = data.borrow_mut(); *value += 1; } println!("القيمة بعد التعديل: {}", data.borrow()); }

في هذا المثال، RefCell يوفر واجهة تسمح بالتعديل الداخلي مع التحقق من القواعد أثناء وقت التشغيل وليس وقت الترجمة. إذًا Rc يشارك الملكية، وRefCell يسمح بالتعديل.

قيود ومخاطر استخدام Rc

رغم فوائد Rc العديدة في إدارة الملكية المشتركة، إلا أن هناك بعض المخاطر والقيود التي يجب الانتباه إليها:

  • الحلقات المرجعية (Reference Cycles): إذا قمت بإنشاء حلقة من مؤشرات Rc التي تشير إلى بعضها البعض، فإن عداد المراجع لن يصل إلى صفر أبدًا، مما يؤدي إلى تسريب الذاكرة. لذلك، من المهم استخدام Weak لكسر هذه الحلقات.

  • عدم الأمان عبر الخيوط: لا يمكن استخدام Rc بأمان في البرامج متعددة الخيوط.

  • تكلفة حسابية: عداد المراجع في Rc يضيف تكلفة زمنية وذاكرة إضافية مقارنة بالمؤشرات العادية.

الأدوات المرتبطة بـ Rc في مكتبة رست القياسية

  • Rc::clone(&rc): لزيادة عداد المراجع وإنشاء نسخة جديدة من Rc.

  • Rc::strong_count(&rc): لاسترجاع عدد المراجع القوية الحالية إلى البيانات.

  • Rc::weak_count(&rc): لاسترجاع عدد المراجع الضعيفة (Weak references) التي تشير إلى البيانات.

  • Rc::downgrade(&rc): لتحويل Rc إلى Weak.

جدول مقارنة بين Rc و Weak

الخاصية Rc Weak
نوع المؤشر قوي (Strong reference) ضعيف (Weak reference)
تأثير على عداد المراجع يزيد العداد ويمنع تحرير البيانات لا يزيد العداد، لا يمنع تحرير البيانات
قابلية الوصول دائمًا يمكن الوصول إلى البيانات يمكن أن تصبح البيانات غير متاحة إذا انتهى Rc
الاستخدام المشاركة في ملكية البيانات كسر الحلقات المرجعية لتجنب التسريبات
إمكانية التعديل بدون تعديل مباشر لا يمكن تعديل البيانات عبر Weak مباشرة

تأثير Rc على تصميم البرامج في رست

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

وبفضل دعمها في قواعد اللغة، فإن أي محاولة لاستخدام Rc بشكل غير صحيح ستُكتشف وقت الترجمة، مما يقلل فرص ظهور الأخطاء في وقت التشغيل.

تطبيقات عملية لمؤشر Rc

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

كذلك، تستخدم Rc بكثرة في بناء هياكل بيانات مثل القوائم المرتبطة (Linked Lists)، الأشجار، والجرافيك، حيث يحتاج كل عقدة في الهيكل إلى مشاركة المراجع إلى العقد الأخرى بدون خسارة الملكية.

خلاصة

يُعد المؤشر الذكي Rc من الأدوات الأساسية في لغة رست لإدارة ملكية البيانات بشكل مشترك داخل خيط واحد من التنفيذ، ويقوم على مبدأ العد المرجعي لتتبع عدد المراجع إلى بيانات موجودة في الذاكرة. من خلال رفع عدد المراجع أو خفضه تلقائيًا عند إنشاء أو إسقاط المؤشرات، يضمن Rc تحرير الموارد بشكل آمن وفعال، ويمنع أخطاء إدارة الذاكرة التقليدية.

بالرغم من عدم كونه آمنًا عبر الخيوط، يظل Rc أداة قوية ومرنة لبناء برامج تعتمد على مشاركة البيانات غير القابلة للتغيير، مع إمكانية تمديد قدراته بدمجها مع أدوات أخرى مثل RefCell للسماح بالتعديل الداخلي، وWeak لتجنب تسريبات الذاكرة الناتجة عن الحلقات المرجعية.

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


المراجع: