اذهب إلى المحتوى

يُعد المؤشر pointer مفهومًا عامًا لمتغيرٍ يحتوي على عنوان في الذاكرة، ويشير هذا العنوان أو "يؤشر إلى" بعض البيانات الأخرى. أكثر أنواع المؤشرات شيوعًا في رست هو المرجع reference، الذي تعلمناه سابقًا. يُحدّد المرجع بالرمز "&" وتُستعار القيمة التي يشير إليها، ولا يوجد للمؤشرات أي قدرات خاصة عدا الإشارة إلى البيانات، ولا يتطلّب استخدامها أي حِمل إضافي overhead.

من جهة أخرى، تُعدّ المؤشرات الذكية smart pointers هياكل بيانات تعمل مثل مؤشر ولكن لها أيضًا بيانات وصفية metadata وقدرات إضافية، إذ لا يقتصر مفهوم المؤشرات الذكية على رست، فهي نشأت في لغة سي بلس بلس C++‎ وتوجد بلغات أخرى أيضًا. تحتوي رست على مجموعة متنوعة من المؤشرات الذكية المعرَّفة في المكتبة القياسية التي تقدم وظائف أكثر من تلك التي توفرها المراجع، وللتعرف على المفهوم العام، سنلقي نظرةً على بعض الأمثلة المختلفة للمؤشرات الذكية، بما في ذلك نوع مؤشر ذكي لعدّ المراجع reference counting. يمكّنك هذا المؤشر من السماح بوجود عدّة مالكين owners للبيانات من خلال تتبع عددهم، ويُحرّر البيانات في حال لم يتبقَّ أي مالكين.

يوجد بمفهوم رست للملكية والاستعارة فرقٌ إضافي بين المراجع والمؤشرات الذكية؛ إذ بينما تستعير المراجع البيانات فقط، تمتلك المؤشرات الذكية في كثير من الحالات البيانات التي تشير إليها.

صادفنا مسبقًا بعض المؤشرات الذكية -على الرغم من أننا لم ندعوها على هذا النحو في ذلك الوقت- بما في ذلك String و <Vec<T، وكلا النوعين مؤشرات ذكية لأنهما يمتلكان بعض الذاكرة و تسمحان لك بالتلاعب بها، إضافةً لوجود بيانات وصفية وإمكانيات أو ضمانات إضافية. تخزّن String على سبيل المثال سعتها على أنها بيانات وصفية ولديها قدرة إضافية لتضمن أن تكون بياناتها دائمًا بترميز UTF-8 صالح.

تُطبَّق عادةً المؤشرات الذكية باستخدام الهياكل، وتنفّذ المؤشرات الذكية على عكس البنية العادية Deref و Drop، إذ تسمح سمة Deref لنسخة instance من هيكل المؤشر الذكي بالتصرف بمثابة مرجع حتى تتمكن من كتابة شيفرتك البرمجية للعمل مع المراجع أو المؤشرات الذكية، بينما تسمح لك سمة Drop بتخصيص الشيفرة التي تُنفَّذ عندما تخرج نسخة المؤشر الذكي عن النطاق، وسنناقش هنا كلًا من السمات traits ونوضح سبب أهميتها للمؤشرات الذكية.

لن يغطي هذا المقال كل مؤشر ذكي موجود بما أن نمط المؤشر الذكي smart pointer pattern هو نمط تصميم عام يستخدم بصورةٍ متكررة في رست. تمتلك العديد من المكتبات مؤشراتها الذكية الخاصة بها، ويمكنك حتى كتابة المؤشرات الخاصة بك. سنغطي المؤشرات الذكية الأكثر شيوعًا في المكتبة القياسية:

  • <Box<T لحجز مساحة خاصة بالقيم على الكومة heap.
  • <Rc<T نوع عدّ مرجع يمكّن الملكية المتعددة.
  • <Ref<T و <RefMut<T اللذين يمكن الوصول إليهما عن طريق <RefCell<T، وهو نمط يفرض قواعد الاستعارة وقت التنفيذ runtime بدلًا من وقت التصريف compile time.

سنغطي بالإضافة إلى ذلك نمط قابلية التغيير الداخلي interior mutability pattern، إذ يعرّض النوع الثابت immutable واجهة برمجية لتعديل قيمة داخلية، كما سنناقش أيضًا دورات المرجع reference cycles، وسنرى كيف بإمكانها تسريب leak الذاكرة وكيفية منعها من ذلك.

دعنا نبدأ.

استخدام المؤشر Box‎ للإشارة إلى البيانات المخزنة على الكومة

يُعد الصندوق "Box" واحدًا من أكثر المؤشرات الذكية وضوحًا وبساطةً، ويُكتب نوعه بالشكل <Box<T. تسمح لك الصناديق أن تخزن البيانات على الكومة بدلًا من المكدس stack، إذ يبقى المؤشر على المكدس الذي يشير بدوره للبيانات الموجودة على الكومة. عد إلى الفصل المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست لمراجعة الفرق بين الكومة والمكدس.

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

  • عندما يكون لديك نوع بحجم غير معروف وقت التصريف وتريد أن تستخدم قيمةً لهذا النوع في سياق يتطلب حجمه المحدد.
  • عندما يكون لديك حجم كبير من البيانات وتريد أن تنقل ملكيتها ولكنك تريد التيقن أن البيانات لن تُنسَخ عندما تفعل هذا.
  • عندما تريد أن تملك قيمةً ما وتهتم فقط أنها من نوع يناسب سمة محددة بدلًا عن كونها من نوع محدد.

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

استخدام Box‎ لتخزين البيانات على الكومة

سنتكلم عن طريقة كتابة Box<T>‎ وكيفية تفاعل هذا النوع مع القيم المخزنة داخله قبل أن نناقش حالة استخدام تخزين الكومة للنوع Box<T>‎.

توضح الشيفرة 1 كيفية استخدام صندوق لتخزين قيمة i32 على الكومة:

اسم الملف: src/main.rs

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

[الشيفرة 1: تخزين قيمة من النوع i32 على الكومة باستعمال صندوق box]

نعرّف المتغير b ليملك القيمة Box التي تشير إلى القيمة "5" المخزنة على الكومة. سيطبع هذا البرنامج "b = 5" وفي هذه الحالة سنصل للبيانات الموجودة في الصندوق بطريقة مشابهة في حال كانت البيانات مخزنة على المكدس. ستُحرَّر القيمة deallocated كما في أي قيمة ممتلكة، عندما يخرج صندوق عن النطاق كما تفعل b في نهاية main، وتحدث عملية التحرير لكل من الصندوق (المخزن على المكدس) والبيانات التي يشير إليها (المخزنة على الكومة).

وضع قيمة وحيدة على الكومة غير مفيد، لأنك لن تستخدم الصناديق بحد ذاتها كثيرًا، ووجود قيم مثل قيمة وحيدة من النوع i32 على المكدس -إذ تُخزن افتراضيًا هناك- مناسبٌ أكثر في أغلب الحالات. لننظر إلى حالة تسمح لنا الصناديق أن نعرّف أنواع لن يُسمح لنا بتعريفها إن لم يكن لدينا صناديق.

تمكين الأنواع التعاودية باستخدام الصناديق

يمكن للقيمة من نوع تعاودي recursive type أن تملك قيمةً أخرى من النوع ذاته مثل جزء من نفسها. تمثّل الأنواع التعاودية مشكلة إذ أن رست تحتاج لمعرفة المساحة التي يحتلها نوع ما وقت التصريف، ويمكن لتداخل قيم الأنواع التعاودية نظريًا أن يستمر إلى ما لا نهاية، لهذا لا يمكن أن تعرف رست كم تحتاج القيمة من مساحة، إلا أنه يمكننا استخدام الأنواع التعاودية بإدخال صندوق في تعريف النوع التعاودي نظرًا لأن الصناديق لها حجم معروف.

لنكتشف قائمة البنية "cons list" مثالًا على نوع تعاودي، إذ أن نوع البيانات هذا موجود كثيرًا في لغات البرمجة الوظيفية. يُعَدّ نوع قائمة البنية بسيطًا وواضحًا باستثناء نقطة التعاود فيه، وبالتالي ستكون المفاهيم في الأمثلة التي سنعمل عليها مفيدةً في أي وقت ستصادف فيه حالات أكثر تعقيدًا من ضمنها الأنواع التعاودية.

المزيد من المعلومات عن قائمة البنية

تُعد قائمة البنية هيكل بيانات أتى من لغة البرمجة ليسب Lisp وشبيهاتها، وتتألف من أزواج متداخلة، وهي نسخة ليسب من القائمة المترابطة linked list، ويأتي اسم هيكل البيانات هذا من الدالة cons (اختصارًا لدالة البنية construct function) في ليسب التي تبني بدورها زوجًا جديدًا من وسيطين arguments. يمكننا بناء قوائم بنية مؤلفة من أزواج تعاودية عن طريق استدعاء cons على زوج يحتوي على قيمة وزوج آخر.

إليك المثال التوضيحي pseudocode لقائمة بنية تحتوي على القائمة 1، 2، 3 مع وجود كل زوج داخل قوسين:

(1, (2, (3, Nil)))

يحتوي كل عنصر في قائمة البنية على عنصرين: القيمة للعنصر الحالي والعنصر التالي، إلا أن العنصر الأخير في القائمة يحتوي فقط على قيمة تُدعى Nil دون عنصر تالي. يمكن إنشاء قائمة البنية عن طريق استدعاء دالة cons بصورة تعاودية، والاسم المتعارف عليه للدلالة على الحالة الأساسية base case للتعاودية هو Nil، مع العلم أنه ليس خاضعًا لنفس مبدأ المصطلحين "null" أو "nil" الذين ناقشناهما سابقًا، فهما يمثلان مؤشرًا على قيمة غير موجودة أو غير صالحة.

لا تعد قائمة بينة من هياكل البيانات المُستخدمة بكثرة في رست، إذ يُعد النوع <Vec<T خيارًا أفضل للاستعمال في معظم الوقت عندما تملك قائمة عناصر في رست، كما يوجد أنواع أخرى لبيانات تعاودية مفيدة في حالات متعددة، لكن من خلال البدء بقائمة البنية هنا، سنتعرف كيف تمكّننا للصناديق من تعريف نوع بيانات تعاودية دون ارتباك.

تتضمن الشيفرة 2 على تعريف لمعدّد enum لقائمة بنية. لاحظ أن هذه الشيفرة لن تُصرَّف بعد لأن النوع List لا يملك حجمًا محددًا وهذا ما سنوضحه لاحقًا.

اسم الملف: src/main.rs

enum List {
    Cons(i32, List),
    Nil,
}

[الشيفرة 2: المحاولة الأولى لتعريف معدّد لتمثيل هيكل البيانات قائمة البنية لقيم من النوع i32]

ملاحظة: نطبّق قائمة البنية التي تحمل فقط قيم من النوع i32 بهدف التوضيح، إذ يمكننا تنفيذها باستعمال الأنواع المعمّمة generics كما ناقشنا سابقًا وذلك لتعريف نوع قائمة بنية يخزّن قيمًا من أي نوع.

يبدو استعمال النوع List لتخزين القائمة "1‎, 2, 3" كما توضح الشيفرة 3:

اسم الملف: src/main.rs

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

[الشيفرة 3: استعمال المعدّد List لتخزين القائمة "1‎, 2, 3"]

تحمل قيمة Cons الأولى على "1" وقيمة List أخرى، قيمة List هذه هي قيمة Cons أخرى تحتوي على "2" وقيمة List أخرى، قيمة List هذه هي قيمة Cons أخرى تحتوي على "3" وقيمة List التي هي في النهاية Nil ألا وهو المتغاير variant غير التعاودي الذي يشير إلى نهاية القائمة.

إذا حاولنا تصريف الشيفرة البرمجية الموجودة في الشيفرة 3، فسنحصل على الخطأ الموضح في الشيفرة 4:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

For more information about this error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to previous error

[الشيفرة 4: الخطأ الذي نحصل عليه عندما نحاول تعريف معدّد تعاودي]

يُظهِر الخطأ أن هذا النمط "له حجم لا نهائي"، والسبب هو تعريفنا للنوع List بمتغير تعاودي، أي أنه يحمل قيمةً أخرى لنفسه مباشرًة، ونتيجة لذلك، لا تستطيع رست معرفة مقدار المساحة التي يحتاجها لتخزين قيمة List. دعونا نوضح لماذا نحصل على هذا الخطأ. أولًا، سننظر إلى كيفية تحديد رست لمقدار المساحة التي يحتاجها لتخزين قيمة لنوع غير تعاودي.

حساب حجم نوع غير تعاودي

تذكر معدّد Message الذي عرّفناه سابقًا (الشيفرة 2 من الفصل التعدادات enums في لغة رست Rust)عندما ناقشنا تعريفات المعدّد:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

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

قارن هذا مع ما يحدث عندما تحاول رست تحديد مقدار المساحة التي يحتاجها نوع تعاودي مثل المعدد List في الشيفرة 2، إذ يبدأ المصرّف بالنظر إلى المتغاير Cons الذي يحمل قيمةً من النوع i32 وقيمةً من النوع List، لذلك يحتاج Cons إلى مساحة مساوية لحجم النوع i32 إضافةً إلى حجم النوع List. لمعرفة مقدار الذاكرة التي يحتاجها النوع List ينظر المصرّف إلى المتغايرات بدءًا من المتغاير Cons، الذي يحمل قيمةً من النوع i32 وقيمةً من النوع List، وتستمر هذه العملية لما لا نهاية، كما هو موضح في الشكل 1.

15-01.png

[الشكل 1: List لانهائية مؤلفة من متغايرات Cons لانهائية]

استخدام <Box<T للحصول على نوع تعاودي بحجم معروف

يعطينا المصرّف الخطأ التالي لأن رست لا يمكنها معرفة مقدار المساحة المراد تخصيصها لأنواع معرّفة بصورةٍ تعاودية مرفقًا مع هذا الاقتراح المفيد:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

يعني "التحصيل indirection" -في هذا الاقتراح- أنه بدلًا من تخزين قيمة مباشرةً، يجب علينا تغيير هيكل البيانات المُستخدَم لتخزين القيمة بصورةٍ غير مباشرة عن طريق تخزين مؤشر يشير إلى القيمة عوضًا عن ذلك.

نظرًا لأن <Box<T هو مؤشر فإن رست تعرف دائمًا مقدار المساحة التي يحتاجها <Box<T، إذ أن حجم المؤشر لا يتغير بناءً على كمية البيانات التي يشير إليها، وهذا يعني أنه يمكننا وضع <Box<T داخل المتغاير Cons بدلًا من قيمة List أخرى مباشرةً. سيشير <Box<T إلى قيمة List التالية التي ستكون على الكومة بدلًا من داخل المتغاير Cons. نظريًا، لا يزال لدينا قائمة أنشئت باستخدام قوائم تحتوي على قوائم أخرى، ولكن هذا التطبيق الآن أشبه بوضع العناصر بجانب بعضها بدلًا من وضع بعضها داخل الأخرى.

يمكننا تغيير تعريف معدّد List في الشيفرة 2 باستخدام List في الشيفرة 3 كما هو موضح في الشيفرة 5 التي ستصرَّف بنجاح:

اسم الملف: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

[الشيفرة 5: تعريف List التي تستخدم <Box<T للحصول على حجم معروف]

يحتاج المتغاير Cons إلى حجمi32 بالإضافة إلى مساحة لتخزين بيانات مؤشر الصندوق، وبما أن المتغاير Nil لا يخزن أي قيم فهو يحتاج إلى مساحة أقل من المتغاير Cons. نعلم الآن أن أي قيمة List ستشغل حجم i32 إضافةً إلى حجم بيانات مؤشر الصندوق. كسرنا السلسلة اللانهائية التعاودية باستخدام الصندوق، بحيث يمكن للمصرف الآن معرفة الحجم الذي يحتاجه لتخزين قيمة List. يوضح الشكل 2 ما يبدو عليه متغاير Cons الآن.

15-02.png

[الشكل 2: List ذات حجم محدد لأن Cons يحمل Box]

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

النوع <Box<T هو مؤشر ذكي لأنه يطبق السمة Deref التي تسمح لقيم <Box<T أن تُعامَل بمثابة مراجع. عندما تخرج قيمة <Box<T عن النطاق، تُمسَح بيانات الكومة التي يشير إليها الصندوق بسبب تطبيق السمة Drop. ستبرز أهمية هاتان السمتان أكثر عندما نناقش أنواع المؤشرات الذكية الأخرى لاحقًا. لنكتشف هاتين السمتين بتفاصيل أكثر.

ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...