Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

استخدام Box<T> للإشارة إلى البيانات في الكومة (Using Box<T> to Point to Data on the Heap)

أكثر (المؤشرات الذكية) smart pointers وضوحاً هو الصندوق، والذي يُكتب نوعه Box<T>. تسمح لك الصناديق بتخزين البيانات في (الكومة) heap بدلاً من (المكدس) stack. ما يتبقى في المكدس هو المؤشر إلى بيانات الكومة. ارجع إلى الفصل الرابع لمراجعة الفرق بين المكدس والكومة.

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

  • عندما يكون لديك نوع لا يمكن معرفة حجمه في وقت التجميع، وتريد استخدام قيمة من ذلك النوع في سياق يتطلب حجماً دقيقاً.
  • عندما يكون لديك كمية كبيرة من البيانات، وتريد نقل (الملكية) ownership ولكن مع ضمان عدم نسخ البيانات عند القيام بذلك.
  • عندما تريد امتلاك قيمة، ويهتم كودك فقط بأنها نوع ينفذ (سمة) trait معينة بدلاً من كونها نوعاً محدداً.

سنوضح الحالة الأولى في “تمكين الأنواع العودية باستخدام الصناديق”. في الحالة الثانية، يمكن أن يستغرق نقل ملكية كمية كبيرة من البيانات وقتاً طويلاً لأن البيانات يتم نسخها في المكدس. لتحسين الأداء في هذه الحالة، يمكننا تخزين الكمية الكبيرة من البيانات في الكومة داخل صندوق. بعد ذلك، يتم نسخ كمية صغيرة فقط من بيانات المؤشر في المكدس، بينما تظل البيانات التي يشير إليها في مكان واحد في الكومة. تُعرف الحالة الثالثة باسم (كائن السمة) trait object ، وقد خُصص قسم “استخدام كائنات السمة للتجريد عبر السلوك المشترك” في الفصل 18 لهذا الموضوع. لذا، فإن ما تتعلمه هنا ستطبقه مرة أخرى في ذلك القسم!

تخزين البيانات في الكومة (Storing Data on the Heap)

قبل أن نناقش حالة استخدام تخزين الكومة لـ Box<T> ، سنغطي الصيغة وكيفية التفاعل مع القيم المخزنة داخل Box<T>.

تعرض القائمة 15-1 كيفية استخدام صندوق لتخزين قيمة i32 في الكومة.

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

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

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

تمكين الأنواع العودية باستخدام الصناديق (Enabling Recursive Types with Boxes)

يمكن أن تحتوي قيمة من (نوع عودي) recursive type على قيمة أخرى من نفس النوع كجزء من نفسها. تشكل الأنواع العودية مشكلة لأن Rust تحتاج إلى معرفة مقدار المساحة التي يشغلها النوع في وقت التجميع. ومع ذلك، فإن تداخل قيم الأنواع العودية يمكن نظرياً أن يستمر إلى ما لا نهاية، لذا لا يمكن لـ Rust معرفة مقدار المساحة التي تحتاجها القيمة. نظراً لأن الصناديق لها حجم معروف، يمكننا تمكين الأنواع العودية عن طريق إدخال صندوق في تعريف النوع العودي.

كمثال على نوع عودي، دعنا نستكشف (قائمة cons) cons list. هذا نوع بيانات يوجد عادة في لغات البرمجة الوظيفية. نوع قائمة cons الذي سنعرفه بسيط باستثناء العودية؛ لذلك، ستكون المفاهيم في المثال الذي سنعمل معه مفيدة في أي وقت تدخل فيه في مواقف أكثر تعقيداً تتضمن أنواعاً عودية.

فهم قائمة Cons (Understanding the Cons List)

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

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

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

يحتوي كل عنصر في قائمة cons على عنصرين: قيمة العنصر الحالي وقيمة العنصر التالي. يحتوي العنصر الأخير في القائمة فقط على قيمة تسمى Nil بدون عنصر تالٍ. يتم إنتاج قائمة cons عن طريق استدعاء دالة cons بشكل عودي. الاسم المتعارف عليه للإشارة إلى الحالة الأساسية للعودية هو Nil. لاحظ أن هذا ليس نفس مفهوم “null” أو “nil” الذي نوقش في الفصل السادس، والذي يمثل قيمة غير صالحة أو غائبة.

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

تحتوي القائمة 15-2 على تعريف (تعداد) enum لقائمة cons. لاحظ أن هذا الكود لن يتم تجميعه بعد، لأن نوع List ليس له حجم معروف، وهو ما سنوضحه.

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

fn main() {}

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

استخدام نوع List لتخزين القائمة 1, 2, 3 سيبدو مثل الكود في القائمة 15-3.

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

// --snip--

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

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

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

إذا حاولنا تجميع الكود في القائمة 15-3، فسنحصل على الخطأ الموضح في القائمة 15-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 {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing when `List` needs drop
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing when `List` needs drop again
  = note: cycle used when computing whether `List` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors

يظهر الخطأ أن هذا النوع “له حجم لانهائي”. والسبب هو أننا عرفنا List بمتغير عودي: فهو يحمل قيمة أخرى من نفسه مباشرة. ونتيجة لذلك، لا تستطيع Rust معرفة مقدار المساحة التي تحتاجها لتخزين قيمة List. دعنا نحلل سبب حصولنا على هذا الخطأ. أولاً، سننظر في كيفية تقرير Rust لمقدار المساحة التي تحتاجها لتخزين قيمة من نوع غير عودي.

حساب حجم نوع غير عودي (Computing the Size of a Non-Recursive Type)

تذكر تعداد Message الذي عرفناه في القائمة 6-2 عندما ناقشنا تعريفات التعداد في الفصل السادس:

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

fn main() {}

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

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

An infinite Cons list: a rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Cons' and a smaller version of the outer 'Cons' rectangle. The 'Cons' rectangles continue to hold smaller and smaller versions of themselves until the smallest comfortably sized rectangle holds an infinity symbol, indicating that this repetition goes on forever.

الشكل 15-1: قائمة List لانهائية تتكون من متغيرات Cons لانهائية

الحصول على نوع عودي بحجم معروف (Getting a Recursive Type with a Known Size)

نظراً لأن Rust لا تستطيع معرفة مقدار المساحة التي يجب تخصيصها للأنواع المعرفة عودياً، فإن المترجم يعطي خطأ مع هذا الاقتراح المفيد:

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

في هذا الاقتراح، تعني (غير المباشرة) indirection أنه بدلاً من تخزين قيمة مباشرة، يجب علينا تغيير هيكل البيانات لتخزين القيمة بشكل غير مباشر عن طريق تخزين مؤشر إلى القيمة بدلاً من ذلك.

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

يمكننا تغيير تعريف تعداد List في القائمة 15-2 واستخدام List في القائمة 15-3 إلى الكود في القائمة 15-5، والذي سيتم تجميعه.

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))))));
}

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

A rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Box' with one inner rectangle that contains the label 'usize', representing the finite size of the box's pointer.

الشكل 15-2: قائمة List ليست لانهائية الحجم، لأن Cons يحمل Box

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

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