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

استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust


Naser Dakhel

الهيكل struct أو البنية structure هو نوع بيانات مُخصّص يسمح لنا باستخدام عدة قيم بأسماء مختلفة في مجموعة واحدة ذات معنًى ما. يشبه الهيكل سمات attributes بيانات الكائن وفقًا لمفهوم البرمجة كائنية التوجه أو OOP.

سنقارن في هذا المقال بين الصفوف tuples والهياكل، ونستعرض كل منها بناءً على ما تعلمته سابقًا، وسنوضح الحالات التي يكون فيها استخدام الهياكل خيارًا أفضل لتجميع البيانات، وكذلك كيفية تعريف وإنشاء الهياكل، إضافةً إلى تعريف الدوال المرتبطة بها وبالأخص الدوال التي تحدد السلوك المرتبط بنوع الهيكل، والتي تُدعى التوابع methods. تُعدّ الهياكل والمُعدّدات enums (سنناقشها لاحقًا) من لبنات بناء نوع بيانات جديد ضمن نطاق برنامجك وذلك للاستفادة الكاملة من خاصية التحقق من الأنواع في رست عند وقت التصريف.

تعريف وإنشاء نسخة من الهياكل

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

نستخدم الكلمة المفتاحية struct لتعريف الهيكل ونُلحقها باسمه، ويجب أن يصِف اسم الهيكل استخدام البيانات التي يجمعها ويحتويها، ومن ثمّ نستخدم الأقواس المعقوصة curly brackets لتعريف أسماء وأنواع البيانات التي يحتويها الهيكل وتُدعى هذه البيانات باسم الحقول fields، فعلى سبيل المثال توضح الشيفرة 1 هيكلًا يحتوي داخله معلومات تخص معلومات عن حساب مستخدم.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

[الشيفرة 1: تعريف الهيكل User]

نُنشئ نسخةً instance من الهيكل الذي عرّفناه حتى نستطيع استخدامه، ومن ثم نحدّد قيمًا لكل حقل ضمنه. نستطيع إنشاء نسخة من الهيكل عن طريق ذكر اسم الهيكل ومن ثم إضافة أقواس معقوصة تحتوي على ثنائيات من التنسيق key: value، إذ يمثّل المفتاح key اسم الحقل، بينما تمثّل القيمة value البيانات التي نريد تخزينها في ذلك الحقل، وليس من الواجب علينا ذكر الحقول بالترتيب المذكور في تصريح الهيكل؛ أي بكلمات أخرى، يُعدّ تعريف الهيكل بمثابة قالب عام للنوع وتملأ النُسخ ذلك القالب ببيانات معينة لإنشاء قيمة من نوع الهيكل، ويمكن مثلًا التصريح عن مستخدم ما كما هو موضح في الشيفرة 2.

fn main() {
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
}

[الشيفرة 2: إنشاء نسخة من الهيكل User]

نستخدم النقطة (.) للحصول على قيمة محددة من هيكل ما؛ فإذا أردنا مثلًا الحصول على البريد الإلكتروني الخاص بالمستخدم فقط، فيمكننا كتابة user1.email عندما نريد استخدام تلك القيمة؛ وإذا كانت النسخة تلك قابلة للتعديل mutable، فيمكننا تغيير القيمة باستخدام الطريقة ذاتها أيضًا وإسناد الحقل إلى قيمة جديدة. توضح الشيفرة 3 كيفية تغيير القيمة في الحقل email الخاصة بالهيكل User القابل للتعديل.

fn main() {
    let mut user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

[الشيفرة 3: تعديل قيمة الحقل email الخاصة بنسخة من الهيكل User]

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

توضح الشيفرة 4 الدالة build_user التي تُعيد نسخةً من الهيكل User باستخدام اسم مستخدم وبريد إلكتروني يُمرّران إليها، إذ يحصل الحقل active على القيمة true، بينما يحصل الحقل sign_in_count على القيمة "1".

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

[الشيفرة 4: الدالة build_user التي تأخذ بريد إلكتروني واسم مستخدم ومن ثمّ تُعيد نسخة من الهيكل User]

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

ضبط قيمة حقول الهيكل بطريقة مختصرة

يُمكننا استخدام طريقة مختصرة في ضبط قيمة حقول الهيكل بما أن أسماء معاملات الدالة وأسماء حقول الهيكل متماثلة في الشيفرة 4، ولاستخدام هذه الطريقة نُعيد كتابة الدالة build_user بحيث تؤدي الغرض ذاته دون تكرار أي من أسماء الحقول email و username كما هو موضح في الشيفرة 5.

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

[الشيفرة 5: دالة build_user تستخدم طريقة إسناد قيم الحقول المختصر لأن لمعاملات email و username الاسم ذاته لحقول الهيكل]

نُنشئ هنا نسخةً جديدةً من الهيكل User الذي يحتوي على هيكل يدعى email، ونضبط قيمة الحقل email إلى قيمة المعامل email المُمرّر إلى الدالة build_user، وذلك لأن للحقل والمعامل الاسم ذاته وبذلك نستطيع كتابة email بدلًا من كتابة email: email.

إنشاء نسخ من نسخ أخرى عن طريق صيغة تحديث الهيكل

من المفيد غالبًا إنشاء نسخة جديدة من الهيكل، الذي يتضمن معظم القيم من نسخةٍ أخرى ولكن مع بعض التغييرات وذلك باستخدام صيغة تحديث الهيكل struct update syntax.

تظهر الشيفرة 6 كيفية إنشاء نسخة مستخدم user جديد في المستخدم user2 بصورةٍ منتظمة دون استخدام صيغة تحديث الهيكل، إذ ضبطنا قيمة جديدة لحقل email بينما استخدمنا نفس القيم للمستخدم user1 المُنشأ في الشيفرة 5.

fn main() {
    // --snip--

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

[الشيفرة 6: إنشاء نسخة user باستخدام إحدى قيم المستخدم user1]

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

fn main() {
    // --snip--

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

[الشيفرة 7: استخدام صيغة تحديث الهيكل لضبط قيمة email لنسخة هيكل المستخدم User واستخدام بقية قيم المستخدم user1]

تنشئ الشيفرة 7 أيضًا نسخةً في الهيكل user2 بنفس قيم الحقول username و active و sign_in_count للهيكل user1 ولكن لها قيمة email مختلفة. يجب أن يأتي المستخدم user1.. أخيرًا ليدل على أن الحقول المتبقية يجب أن تحصل على قيمها من الحقول المقابلة في الهيكل user1، ولكن يمكننا تحديد قيم أي حقل من الحقول دون النظر إلى ترتيب الحقول الموجود في تعريف الهيكل.

تستخدم صيغة تحديث الهيكل الإسناد =، لأنه ينقل البيانات تمامًا كما هو الحال في القسم "طرق التفاعل مع البيانات والمتغيرات: النقل" من مقال الملكية ownership في لغة رست. لم يعد بإمكاننا في هذا المثال استخدام user1 بعد إنشاء user2 بسبب نقل السلسلة النصية String الموجودة في الحقل username من المستخدم user1 إلى user2. إذا أعطينا قيم سلسلة نصية جديدة إلى user2 لكلا الحقلين username و email واستخدمنا فقط قيم الحقلين active و sign_in_count من الهيكل user1، سيبقى الهيكل user1 في هذه الحالة صالحًا بعد إنشاء user2. تكون أنواع بيانات الحقلين active و sign_in_count بحيث تنفّذ السمة Copy، وبالتالي سينطبق هنا الأسلوب الذي ناقشناه في القسم الأول من الفصل المشار إليه آنفًا.

استخدام هياكل الصفوف دون حقول مسماة لإنشاء أنواع مختلفة

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

لتعريف هيكل صف، نبدأ بالكلمة المفتاحية struct ومن ثم اسم الصف متبوعًا بالأنواع الموجودة في الصف. على سبيل المثال، نعرّف هنا هيكلا صف بالاسم Color و Point ونستخدمهما:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

لاحظ أن black و origin من نوعين مختلفين، وذلك لأنهما نسختان من هياكل صف مختلفة، إذ يُعدّ كل هيكل تُعرفه نوعًا مختلفًا حتى لو كانت الحقول داخل الهيكل من نوع مماثل لهيكل آخر، على سبيل المثال، لا يمكن لدالة تأخذ النوع Color وسيطًا أن تأخذ النوع Point على الرغم من أن النوعين يتألفان من قيم من النوع i32. إضافةً لما سبق، يُماثل تصرف هياكل الصفوف تصرف الصفوف؛ إذ يمكنك تفكيكها إلى قطع متفرقة أو الوصول إلى قيم العناصر المختلفة بداخلها عن طريق استخدام النقطة (.) متبوعةً بدليل العنصر، وهكذا.

الهياكل الشبيهة بالوحدات بدون أي حقول

يُمكنك أيضًا تعريف هياكل لا تحتوي على أي حقول، وتدعى الهياكل الشبيهة بالوحدات unit-like structs لأنها مشابهة لنوع الوحدة unit type () الذي تحدثنا عنه سابقًا. يُمكن أن نستفيد من الهياكل الشبيهة بالوحدات عندما نريد تطبيق سمة trait على نوع ما ولكننا لا نملك أي بيانات نريد تخزينها داخل النوع، وسنناقش مفهوم السمات لاحقًا.

إليك مثالًا عن تصريح وإنشاء نسخة من هيكل شبيه بالوحدات يدعى AlwaysEqual:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

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

ملكية بيانات الهيكل

استخدمنا في الشيفرة 1 النوع String في الهيكل User الموجود بدلًا عن استخدام نوع شريحة سلسلة نصية string slice type بالشكل ‎&str، وهذا استخدام مقصود لأننا نريد لكل نسخة من هذا الهيكل أن تمتلك جميع بياناتها وأن تكون بياناتها صالحة طالما الهيكل بكامله صالح.

من الممكن للهياكل أيضًا أن تُخزِّن مرجعًا لبيانات يمتلكها شيء آخر، إلا أن ذلك يتطلب استخدام دورات الحياة lifetimes وهي ميزة في رست سنتحدث عنها لاحقًا، إذ تضمن دورات الحياة أن البيانات المُشار إليها باستخدام الهيكل هي بيانات صالحة طالما الهيكل صالح. دعنا نفترض أنك تريد تخزين مرجع في هيكل ما دون تحديد دورات الحياة كما يلي (لن ينجح الأمر):

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

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

سيشكو المصرّف ويخبرك أنه بحاجة محدّدات دورات الحياة lifetime specifiers:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` due to 2 previous errors

سنناقش كيفية حل هذه المشكلة لاحقًا، بحيث يمكنك تخزين المراجع في الهياكل، إلا أننا سنستخدم حاليًا الأنواع المملوكة owned types مثل String بدلًا عن المراجع مثل ‎&str لتجنب هذه المشكلة.

مثال على برنامج يستخدم الهياكل

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

نُنشئ مشروعًا ثنائيًا جديدًا باستخدام كارجو Cargo باسم "rectangles"، ويأخذ هذا البرنامج طول وعرض المستطيل بالبيكسل pixel ويحسب مساحة المستطيل. توضح الشيفرة 8 برنامجًا قصيرًا ينفذ ذلك بإحدى الطرق ضمن الملف "src/main.rs".

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

[الشيفرة 8: حساب مساحة المستطيل المُحدّد بمتغيّرَي الطول والعرض]

دعنا الآن نُنفّذ البرنامج باستخدام الأمر cargo run:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

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

تكمن المشكلة في شيفرتنا البرمجية الحالية في بصمة signature الدالة area:

fn area(width: u32, height: u32) -> u32 {

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

إعادة كتابة البرنامج باستخدام الصفوف

توضح الشيفرة 9 إصدارًا آخر من برنامجنا باستخدام الصفوف.

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

[الشيفرة 9: تخزين طول وعرض المستطيل في صف]

البرنامج الحالي أفضل، إذ تسمح لنا الصفوف بتنظيم القيم على نحوٍ أفضل، إضافةً إلى أننا نمرّر للدالة الآن معاملًا واحدًا، إلا أن هذا الإصدار أقل وضوحًا، لأن الصفوف لا تُسمّي عناصرها وبالتالي علينا استخدام دليل العنصر للوصول إلى القيمة التي نريدها مما يجعل من حساباتنا أقل وضوحًا.

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

إعادة كتابة البرامج باستخدام الهياكل وبوضوح أكبر

نستخدم الهياكل حتى نجعل من بياناتنا ذات معنى بتسميتها، إذ يمكننا تحويل الصف المُستخدَم سابقًا إلى هيكل باسم له، إضافةً إلى اسمٍ لكل حقلٍ داخله كما هو موضح في الشيفرة 10.

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

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

[الشيفرة 10: تعريف الهيكل Rectangle]

عرّفنا هنا هيكلًا اسمه Rectangle، ثم عرّفنا داخل الأقواس المعقوصة حقلَي الهيكل width و height من النوع u32، وننشئ بعدها نسخةً من الهيكل ضمن الدالة main بعرض 30 بيكسل وطول 50 بيكسل.

الدالة area في هذا الإصدار من البرنامج معرّفة بمعامل واحد، وقد سمّيناه rectangle وهو من نوع الهيكل Rectangle القابل للإستعارة دون تعديل، وكما ذكرنا سابقًا، يمكننا استعارة borrow الهيكل بدلًا من الحصول على ملكيته وبهذه الطريقة تحافظ الدالة main على ملكيته ويمكننا استخدامه عن طريق النسخة rect1 وهذا هو السبب في استخدامنا للرمز & في بصمة الدالة وعند استدعائها.

تُستخدَم الدالة area في الوصول لحقلَي نسخة الهيكل Rectangle وهُما width و height، وتدلّ بصمة الدالة هنا على ما نريد فعله بوضوح: احسب مساحة Rectangle باستخدام قيمتَي الحقلين width و height، وهذا يدلّ قارئ الشيفرة البرمجية على أن القيمتين مترابطتين فيما بينهما ويُعطي اسمًا واصفًا واضحًا لكل من القيمتُين بدلًا من استخدام قيم دليل الصف "0" و"1" كما سبق. وهذا الإصدار الأوضح حتى اللحظة.

بعض الإضافات المفيدة باستخدام السمات المشتقة

سيكون من المفيد أن نطبع نسخةً من الهيكل Rectangle عند تشخيص أخطاء برنامجنا للتحقق من قيم الحقول، نُحاول في الشيفرة 11 فعل ذلك باستخدام الماكرو !println كما عهدنا في المقالات السابقة إلا أن هذا الأمر لن ينجح.

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

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);
}

[الشيفرة 11: محاولة طباعة نسخة من الهيكل Rectangle]

نحصل على رسالة الخطأ التالية عندما نُصرّف الشيفرة البرمجية السابقة:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

يمكّننا الماكرو !println من تنسيق الخرج بعدّة أشكال، إلا أن التنسيق الافتراضي هو التنسيق المعروف باسم Display الذي يستخدم أسلوب كتابة الأقواس المعقوصة، وهو تنسيق موجّه لمستخدم البرنامج، وتستخدم أنواع البيانات الأولية primitive التي استعرضناها حتى الآن تنسيق Display افتراضيًا، وذلك لأن هناك طريقةً واحدةً لعرض القيمة "1" -أو أي قيمة نوع أولي آخر- للمستخدم، لكن الأمر مختلف مع الهياكل إذ أن هناك عدّة احتمالات لعرض البيانات التي بداخلها؛ هل تريد الطباعة مع الفواصل أم بدونها؟ هل تريد طباعة الأقواس المعقوصة؟ هل تريد عرض جميع الحقول؟ وبسبب هذا لا تحاول رست تخمين الطريقة التي نريد عرض الهيكل بها ولا يوجد أي تطبيق لطباعة الهيكل باستخدام النمط Display في الماكرو println!‎ باستخدام الأقواس المعقوصة {}.

إذا قرأنا رسالة الخطأ نجد الملاحظة المفيدة التالية:

   = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

تخبرنا الملاحظة أنه يجب علينا استخدام تنسيق محدّد لطباعة الهيكل، لنجرّب ذلك! يصبح استدعاء الماكرو println!‎ بالشكل التالي:

println!("rect1 is {:?}, rect1);‎

يدّل المحدد ?:‎ داخل الأقواس المعقوصة على أننا نريد استخدام تنسيق طباعة يدعى Debug، إذ يُمكّننا هذا النمط من طباعة الهيكل بطريقة مفيدة للمطورين عند تشخيص أخطاء الشيفرة البرمجية.

صرّف الشيفرة البرمجية باستخدام التغيير السابقة، ونفّذ البرنامج. حصلنا على خطأ من جديد.

error[E0277]: `Rectangle` doesn't implement `Debug`

إلا أن المصرف يساعدنا مجددًا بملاحظة مفيدة:

   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

لا تُضمّن رست إمكانية طباعة المعلومات المتعلقة بتشخيص الأخطاء، إذ عليك أن تحدّد صراحةً أنك تريد استخدام هذه الوظيفة ضمن الهيكل الذي تريد طباعته، ولفعل ذلك نُضيف السمة الخارجية ‎#[derive(Debug)]‎ قبل تعريف الهيكل كما هو موضح في الشيفرة 12.

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

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
}

[الشيفرة 12: إضافة سمة للهيكل للحصول على السمة المُشتقة Debug وطباعة نسخة من الهيكل Rectangle باستخدام تنسيق تشخيص الأخطاء]

لن نحصل على أي أخطاء أخرى عندما ننفذ البرنامج الآن، وسنحصل على الخرج التالي:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

رائع، فعلى الرغم من أن تنسيق الخرج ليس جميلًا إلا أنه يعرض لنا جميع حقول النسخة، وهذا سيساعدنا بالتأكيد خلال عملية تشخيص الأخطاء. من المفيد استخدام المُحدد {?#:} بدلًا من المحدد {?:} عندما يكون لدينا هياكل ضخمة، ونحصل على الخرج بالطريقة التالية عند استخدام المحدد السابق:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

يمكننا طباعة القيم بطريقة أخرى، وهي باستخدام الماكرو dbg!‎، إذ يأخذ هذا الماكرو ملكية التعبير ويطبع الملف ورقم السطر الذي ورد فيه الماكرو، إضافةً إلى القيمة الناتجة من التعبير، ثمّ يُعيد الملكية للقيمة.

ملاحظة: يطبع استدعاء الماكرو dbg!‎ الخرج إلى مجرى أخطاء الطرفية القياسي standard error console stream أو كما يُدعى "stderr" على عكس الماكرو println!‎ الذي يطبع الخرج إلى مجرى خرج الطرفية القياسي standard output console stream أو كما يُدعى "stdout"، وسنتحدث بصورةٍ موسعة عن "stderr" و "stdout" لاحقًا.

إليك مثالًا عمّا سيبدو عليه برنامجنا إذا كُنّا مهتمين بمعرفة القيمة المُسندة إلى الحقل width إضافةً إلى قيم كامل الهيكل في النسخة rect1:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

يُمكننا كتابة dbg!‎ حول التعبير 30‎ *‎ scale وسيحصل الحقل width على القيمة ذاتها في حالة عدم استخدامنا لاستدعاء dbg!‎ هنا لأن dbg!‎ يُعيد الملكية إلى قيمة التعبير، إلا أننا لا نريد للماكرو dbg!‎ أن يأخذ ملكية rect1، لذلك سنستخدم مرجعًا إلى rect1 في الاستدعاء الثاني. إليك ما سيبدو عليه الخرج عند تنفيذ البرنامج:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

يُمكننا ملاحظة أن الجزء الأول من الخرج طُبِعَ نتيجةً للسطر البرمجي العاشر ضمن الملف src/main.rs، إذ أردنا تشخيص الخطأ في التعبير 30‎ *‎ scale والقيمة 60 الناتجة عنه (يطبع تنسيق Debug فقط القيمة في حالة الأعداد الصحيحة)، بينما يطبع الاستدعاء الثاني للماكرو dbg!‎ الوارد في السطر الرابع عشر ضمن الملف src/main.rs قيمة ‎&rect1 الذي هو بدوره نسخةٌ من الهيكل Rectangle، ويستخدم الخرج تنسيق الطباعة Debug ضمن النوع Rectangle. يُمكن للماكرو dbg!‎ أن يكون مفيدًا في العديد من الحالات التي تريد فيها معرفة ما الذي تفعله شيفرتك البرمجية.

تزوّدنا رست بعدد من السمات الأخرى بجانب السمة Debug، ونستطيع استخدامها بواسطة السمة derive مما يُمكّننا من إضافة سلوكيات مفيدة إلى أنواع البيانات المُخصصة، نوضح السمات وسلوكياتها في الملحق (ت)، وسنغطي كيفية إنشاء سمات مُخصصة لاحقًا. هناك العديد من السمات الأخرى بجانب derive، للحصول على معلومات أكثر انظر Attributes.

الدالة area الموجودة لدينا الآن ضيقة الاستخدام، إذ أنها تحسب فقط مساحة المستطيل ومن المفيد أن نربط سلوكها بصورةٍ أكبر مع الهيكل Rectangle لأنها لا تعمل مع أي نوع آخر، وسننظر لاحقًا إلى إصدارات أخرى من هذا البرنامج التي ستحوّل الدالة area إلى تابع area ضمن النوع Rectangle.

‎ترجمة -وبتصرف- لقسم من الفصل Using Structs to Structure Related Data من كتاب 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.


×
×
  • أضف...