التوابع 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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.