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

استخدام التوابع methods ضمن الهياكل structs في لغة رست Rust


Naser Dakhel

التوابع methods مشابهة للدوال functions، إذ نُصرّح عنها باستخدام الكلمة المفتاحية fn متبوعةً باسم التابع، ويمكن للتوابع أن تمتلك عدّة معاملات وأن تُعيد قيمةً ما، ويحتوي التابع بداخله على جزء من شيفرة برمجية تعمل عند استدعاء التابع في مكان آخر، إلا أن التوابع -على عكس الدوال- تُعرّف داخل الهيكل (أو داخل المعدّد enum، أو كائن سمة trait object وهو ما سنتكلم عنه لاحقًا)، ويكون المعامل الأول دائمًا هو self الذي يمثّل نسخةً من الهيكل التي يُستدعى التابع منها.

تعريف التوابع

دعنا نُعدّل الدالة area -في المقال السابق- التي تأخذ نسخةً من الهيكل Rectangle معاملًا لها، ونُنشئ بدلًا من ذلك تابع area معرّف داخل الهيكل Rectangle كما هو موضح في الشيفرة 13.

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

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

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

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

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

[الشيفرة 13: تعريف التابع area داخل الهيكل Rectangle]

نبدأ بكتابة كتلة تطبيق implementation‏ impl داخل الهيكل Rectangle حتى نستطيع تعريف الدالة داخل سياق الهيكل، وسيكون كل شيء ضمن هذه الكتلة مرتبطًا بالنوع Rectangle، من ثمّ ننقل الدالة area إلى داخل أقواس الكتلة impl ونعدّل المعامل الأول -وفي هذه الحالة هو المعامل الوحيد- ليصبح self في بصمة الدالة وأي مكان آخر ضمنها. ننتقل إلى الدالة main وعوضًا عن استدعاء الدالة area وتمرير rect1 مثل وسيط، سنستخدم طريقة كتابة التابع لاستدعاء التابع area على نسخة الهيكل Rectangle، إذ تتلخّص الطريقة بإضافة نقطة (.) بعد نسخة الهيكل متبوعةً باسم التابع ومن ثم القوسين وبداخلهما أي وسطاء.

نستخدم ‎&self في بصمة الدالة area عوضًا عن rectangle: &Rectangle، وفي الحقيقة ‎&self هو اختصار إلى self: &Self، ويمثّل Self داخل الكتلة impl اسمًا مستعارًا للنوع الذي يحتوي داخله الكتلة impl،وهي في هذه الحالة Rectangle. يجب على التوابع أن تحتوي على وسيط باسم self من النوع Self مثل مُعامل أوّل، إذ تسمح لك رست باختصار الاسم إلى self في المعامل الأول للتابع، لاحظ أنّنا ما زلنا بحاجة الرمز & أمام الاسم المختصر self وذلك للإشارة إلى أن التابع يستعير نسخةً من النوع Self كما فعلنا سابقًا باستخدام rectangle: &Rectangle. يمكن للتوابع أن تمتلك الاسم self أو أن تستعير self على نحوٍ غير قابل للتعديل -كما فعلنا هنا- أو أن تستعير self مع إمكانية تعديل كما هو الحال في أي مُعامل آخر.

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

السبب الرئيسي في استخدامنا التوابع بدلًا من الدوال هو التنظيم، إضافةً إلى أننا لا نُكرّر نوع الوسيط self في كل بصمةٍ للتابع، إذ سمحت لنا التوابع بوضع جميع الأشياء التي يمكننا فعلها ضمن نسخة النوع داخل كتلة impl واحدة عوضًا عن إجبار قارئ الشيفرة البرمجية بالبحث عن الدوال التي تشير لإمكانيات الهيكل Rectangle بنفسه في أماكن مختلفة من المكتبة التي كتبناها.

لاحظ أنه يمكننا اختيار اسم التابع على نحوٍ مماثل لاسم حقول الهيكل، على سبيل المثال يمكننا تعريف تابع داخل الهيكل Rectangle باستخدام اسم الحقل width:

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

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

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

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

نختار هنا أن نجعل التابع width يُعيد القيمة true إذا كانت القيمة width في نسخة الهيكل أكبر من 0، وإلا فالقيمة false إذا كانت القيمة مساوية إلى الصفر، ويمكننا الاستفادة من الحقل داخل التابع الذي يحمل الاسم ذاته لأي هدف كان، ثم ننتقل إلى الدالة main ونستخدم التابع بالشكل rect1.width مع الأقواس حتى تعلم رست أننا نقصد التابع width، إذ ستعلم رست أننا نقصد الحقل width، إذا لم نستخدم الأقواس.

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

أين العامل '<-' ؟

لدى لغة سي C و C++‎ معاملان مختلفان لاستدعاء التوابع، إذ يُمكنك استخدام . إذا أردت استدعاء التابع على الكائن مباشرةً، أو استخدام المعامل <- إذا أردت استدعاء التابع على مؤشر يُشير إلى الكائن وتريد أن تحصل dereference على المؤشر عن الكائن أولًا؛ أي بكلمات أخرى، إذا كان object مؤشرًا فكتابة object->something()‎ مشابهة إلى ‎(‎*object).something()‎.

لا يوجد في رست مُكافئ للمعامل <-، بل لدى رست ميزة تُدعى بالمرجع لعنوان الذاكرة والتحصيل التلقائي automatic referencing and dereferencing بدلًا من ذلك، واستدعاء التوابع هو واحدة من الأجزاء في لغة رست التي تتبع هذا السلوك.

إليك كيفية عمل هذه الميزة: عند استدعاء التابع باستخدام object.something()‎ تُضيف رست & أو ‎&mut أو * تلقائيًا حتى يُطابق object بصمة التابع، أي بكلمات أخرى، السطرين البرمجيين متماثلين، إلا أن السطر البرمجي الأول يبدو أكثر ترتيبًا:

p1.distance(&p2);
(&p1).distance(&p2);

يعمل سلوك المرجع لعنوان الذاكرة التلقائي لأن للتوابع مستقبل receiver واضح ألا وهو النوع self، وتستطيع رست باستخدام المستقبل الواضح واسم التابع أن تعرف دون شك إذا ما كان التابع يقرأ (‎&self) أو يعدّل (‎&mut self) أو يستهلك (self)، وتُعدّ حقيقة أن رست تجعل من الاستعارة مباشرة لمستقبل التابع من أهم أجزاء ميزة الملكية في رست.

التوابع التي تحتوي على عدة معاملات

دعنا نتدرب على استخدام التوابع بتطبيق تابع ثاني ضمن الهيكل Rectangle، ونريد في هذه المرة أن تأخذ نسخةٌ من الهيكل Rectangle نسخةً أخرى من الهيكل ذاته وأن تُعيد true إذا كانت النسخة الثانية تتسع كاملةً داخل النسخة الأولى (self) وإلا فيجب أن تُعيد false، وسنستطيع كتابة الشيفرة 14 بعد تعريف التابع can_hold.

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

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

[الشيفرة 14: استخدام التابع can_hold الذي لم نكتبه بعد]

سيبدو خرج الشيفرة البرمجية السابقة كما يلي، وذلك لأن كلا أبعاد النسخة rect2 أصغر من أبعاد النسخة rect1 إلا أن أبعاد النسخة rect3 أكبر من النسخة rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

نعلم أننا نريد تعريف تابع، ولذلك سنكتب ذلك ضمن الكتلة impl Rectangle، وسيكون اسم التابع can_hold وسيستعير نسخةً من Rectangle غير قابلة للتعديل مثل معامل، ويمكننا معرفة نوع المعامل بالنظر إلى السطر البرمجي الذي سيستدعي التابع، إذ يمرر الاستدعاء rect1.can_hold(&rect2)‎ الوسيط ‎&rect2 وهو نُسخة من الهيكل Rectangle مُستعارة غير قابلة للتعديل، وهذا الأمر منطقي لأننا نريد فقط أن نقرأ بيانات النسخة rect2 ولا حاجة لنا في التعديل عليها مما سيتطلب نسخةً مُستعارةً قابلة للتعديل، إذ نريد هنا أن تحتفظ الدالة main بملكية rect2 حتى نستطيع استخدامها مجددًا بعد استدعاء التابع can_hold.

ستكون القيمة المُعادة من التابع can_hold بوليانية boolean وسيتحقق تطبيقنا فيما إذا كان الطول والعرض الخاص بالمعامل self أكبر من الطول والعرض الخاص بنسخة Rectangle الأخرى. دعنا نُضيف التابع can_hold الجديد إلى كتلة impl الموجودة في الشيفرة 13 كما هو موضح في الشيفرة 15.

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

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

[الشيفرة 15: تطبيق التابع can_hold ضمن الهيكل Rectangle الذي يأخذ نسخةً أخرى من الهيكل Rectangle بمثابة معامل]

سنحصل على الخرج المطلوب عند تشغيل الشيفرة البرمجية ضمن main في الشيفرة 14. يمكن أن تأخذ التوابع عدة معاملات إن أردنا وذلك بإضافتها إلى بصمة التابع بعد المعامل self، وتعمل هذه المعاملات كما تعمل في الدوال عادةً.

الدوال المترابطة

تُدعى جميع الدوال المُعرّفة داخل الكتلة impl بالدوال المترابطة associated functions، وذلك لأنها مرتبطة بالنوع الموجود بعد impl، ويمكننا تعريف الدوال المترابطة التي لا تحتوي على المعامل الأول self (وبالتالي فهي لا تُعدّ توابعًا)، لأننا لا نحتاج إلى نسخة من النوع عند تنفيذها، وقد استخدمنا دالةً مشابهةً لهذه سابقًا، ألا وهي الدالة String::from والمعرّفة داخل النوع String.

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

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

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

تُعد كلمات Self المفتاحية في النوع المُعاد ومتن الدالة بمثابة أسماء مستعارة للنوع الذي يظهر بعد الكلمة المفتاحية impl، والتي هي في حالتنا Rectangle.

نستخدم :: مع اسم الهيكل لاستدعاء الدالة المترابطة، والسطر let sq = Rectangle::square(3)‎ هو مثال على ذلك، ويقع فضاء أسماء namespace هذه الدالة داخل الهيكل، ويُستخدم الرمز :: لكلٍّ من الدوال المترابطة وفضاءات الأسماء المُنشأة من قبل الوحدات modules التي سنناقشها لاحقًا.

كتل impl متعددة

يمكن أن يحتوي كل هيكل على عدّة كُتَل impl، فعلى سبيل المثال الشيفرة 15 مكافئة للشيفرة 16 التالية التي تحتوي على كل تابع داخل كتلة impl مختلفة.

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
   }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

[الشيفرة 16: إعادة كتابة الشيفرة 15 باستخدام كتَل impl متعددة]

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

‎ترجمة -وبتصرف- لقسم من الفصل 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.


×
×
  • أضف...