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

تنتشر الدوال في معظم شيفرات راست البرمجية، وقد رأيت سابقًا واحدةً من أهم الدوال في اللغة ألا وهي دالة main وهي نقطة البداية للكثير من البرامج، كما أنك رأيت أيضًا الكلمة المفتاحية fn التي تسمح لك بالتصريح عن دالةٍ جديدة.

تستخدم شيفرة راست البرمجية نمط الثعبان snake case نمطًا اصطلاحيًا لأسماء الدوال والمتغيرات، إذ تكون الأحرف في هذا النمط جميعها أحرف صغيرة ويفصل ما بين الكلمة والأخرى شرطة سفلية. إليك برنامجًا يحتوي على مثال لتعريف دالة:

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

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

نعرّف الدالة في راست بإدخال الكلمة fn متبوعةً باسم الدالة يليها قوسين هلاليّين parentheses ()، بينما تُخبر الأقواس المعقوفة curly brackets {} المصرف بموضع بداية وانتهاء متن الدالة.

يُمكننا استدعاء أي دالة عرفناها سابقًا بإدخال اسمها متبوعًا بقوسين هلاليّين، وبما أن الدالة another_function مُعرفةٌ في البرنامج، يمكننا استدعائها من داخل الدالة main. لاحظ أننا عرفنا another_function بعد دالة main في الشيفرة البرمجية إلا أنه يمكننا تعريفها قبلها أيضًا، إذ لا تُبالي راست بموضع تعريف الدوال طالما يوجد التعريف داخل النطاق scope الذي استدعيت الدالة منه.

دعنا نبدأ مشروعًا ثنائيًا binary project جديدًا باسم "functions" للنظر إلى الدوال بتعمّق أكبر، وضع مثال "another_function" السابق في ملف "src/main.rs" ونفّذه. يجب أن يظهر لك الخرج التالي:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

تُنفّذ هذه السطور البرمجية بالترتيب التي ظهرت فيه في الدالة main، أي تُطبع الرسالة "Hello, world!‎" أولًا، ثم تُستدعى الدالة another_function وتُطبع رسالتها.

المعاملات

يُمكننا تعريف الدوال بحيث تحتوي على معاملات parameters، وهي متغيرات خاصة تنتمي إلى بصمة الدالة function's signature، ويُمكنك استخدام قيم فعلية لهذه الدالة عند احتوائها على معاملات، وتُدعى هذه القيم بالوسطاء arguments إلا أنه غالبًا ما يُستخدم المصطلحان معامل ووسيط بصورةٍ تبادلية interchangeably لأي من المتغيرات في تعريف الدالة أو القيم الفعلية المُمرّرة للدالة عند استدعائها.

نُضيف معاملًا في هذا الإصدار من الدالة another_function:

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

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

يجب أن تحصل على الخرج التالي عند تجربتك لتشغيل البرنامج:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

يحتوي تصريح الدالة another_function على معامل واحد باسم x وهو من النوع i32، بالتالي يضع الماكرو ‏!println القيمة 5 عند تمريرها إلى الدالة مثل قيمة للمعامل x في تنسيق السلسلة النصية.

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

يجب فصل المعاملات بالفاصلة عند تعريف أكثر من معامل واحد كما هو موضح:

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

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

يُنشئ هذا المثال دالةً باسم print_labeled_measurment بمعاملَين، إذ يسمى المعامل الأول value وهو من النوع i32، بينما يسمى النوع الثاني unit_label وهو من النوع char، وتطبع الدالة نصًّا يحتوي على كل من value و unit_label.

دعنا نجرّب تشغيل الشيفرة البرمجية السابقة، وذلك باستبدال البرنامج الموجود حاليًا في ملف "src/main.rs" لمشروع "function" بالشيفرة البرمجية السابقة، وتشغيل البرنامج باستخدام cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

نحصل على الخرج السابق طالما استُدعيت الدالة بالقيمة "5" للمعامل value والقيمة 'h' للمعامل unit_label.

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

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

التعليمات هي توجيهات تُجري عمليات ما ولا تُعيد قيمةً، بينما تُقيّم التعابير إلى قيمة ناتجة. دعنا ننظر إلى بعض الأمثلة.

استخدمنا في الحقيقة سابقًا كلًا من التعابير والتعليمات، وذلك بإنشاء متغير وإسناد قيمة إليه باستخدام الكلمة المفتاحية let، نجد في الشيفرة 3-1 التعليمة let y = 6;‎.

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

fn main() {
    let y = 6;
}

[الشيفرة 3-1: تعريف دالة main يحتوي على تعليمة واحدة]

تُعدّ تعاريف الدوال تعليمات أيضًا، فالمثال السابق هو تعليمة واحدة بذات نفسه.

لا تُعيد التعليمات أي قيمة، لذلك لا يُمكنك إسناد تعليمة let إلى متغير آخر كما نحاول في المثال التالي، إذ ستحصل على خطأ:

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

fn main() {
    let x = (let y = 6);
}

ستحصل على الخطأ التالي عند محاولتك لتشغيل البرنامج السابق:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  | 

For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 2 previous errors; 1 warning emitted

لا تُعيد التعليمة let y = 6 أي قيمة، لذا لا يوجد هناك أي قيمة لإسنادها إلى x، وهذا الأمر مختلفٌ عن باقي لغات البرمجة مثل سي C وروبي Ruby إذ تُعيد عملية الإسناد في هذه اللغات قيمة الإسناد، وبالتالي يمكنك كتابة التعليمة x = y = 6 بحيث تُسند القيمة 6 إلى كل من x و y إلا أن هذا الأمر لا ينطبق في راست.

تُقيِّم وتركّب التعابير معظم الشيفرة البرمجية التي ستكتبها في راست، خُذ على سبيل المثال تعبير العملية الحسابية 5 + 6، التي تُقيّم إلى القيمة 11. يمكن أن تكون التعابير جزءًا من التعليمات، ففي الشيفرة 3-1 تُمثّل 6 في التعليمة let y = 6;‎ تعبيرًا يُقيّم إلى القيمة 6. يُعد كل من استدعاء الدالة واستدعاء الماكرو وإنشاء نطاق جديد باستخدام الأقواس المعقوفة تعبيرًا، على سبيل المثال:

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

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {y}");
}

التعبير التالي هو جزءٌ يُقيم إلى القيمة 4:

{
    let x = 3;
    x + 1
}

تُسند القيمة فيما بعد إلى y كجزء من تعليمة let، لاحظ أن السطر x + 1 لا يحتوي على فاصلة منقوطة في نهايته مثل معظم الأسطر التي كتبناها لحد اللحظة، وذلك لأن التعابير لا تحتوي على فاصلة منقوطة في النهاية، وإذا أضفت الفاصلة المنقوطة فسيتحول التعبير إلى تعليمة ولن يكون هناك أي قيمة مُعادة حينها. تذكّر ما سبق بينما نتكلم عن القيم المُعادة من الدوال والتعابير لاحقًا.

الدوال التي تعيد قيمة

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

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

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

لا يوجد في الدالة five أي استدعاءات، أو ماكرو، أو حتى تعليمة let، بل فقط الرقم 5، وتلك دالة صالحة في لغة راست. لاحظ أن نوع القيمة المُعادة من الدالة مُحدّد أيضًا بكتابة ‎-> i32. يجب أن تحصل على الخرج التالي إذا جرّبت تشغيل الشيفرة البرمجية:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

تُمثّل القيمة 5 في الدالة five القيمة المُعادة من الدالة، وهذا السبب في تحديدنا لنوع القيمة المعادة بالنوع i32، لكن دعنا ننظر إلى الدالة بتعمُّق أكبر، إذ يوجد جزآن مُهمّان، هما:

أولًا، يوضح السطر let x = five();‎ أننا نستخدم القيمة المُعادة من الدالة لإسنادها مثل قيمة أولية للمتغير وبما أن الدالة تُعيد القيمة 5، فهذا الأمر موافق لكتابة السطر البرمجي التالي تمامًا:

let x = 5;

ثانيًا، لا تحتوي الدالة five أي معاملات وتُعرِّف نوع القيمة المعادة، إلا أن متن الدالة يحتوي على القيمة 5 بصورةٍ منفردة دون فاصلة منقوطة وذلك لأنه تعبير نُريد قيمته على أنها قيمة الدالة المُعادة.

دعنا ننظر إلى مثال آخر:

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

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

سيطبع تنفيذ الشيفرة البرمجية السابقة The value of x is: 6، إلا أننا سنحصل على خطأ إذا استبدلنا الفاصلة المنقوطة في نهاية السطر x + 1 مما يُغيّر السطر من تعبير إلى تعليمة.

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

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

تصريف الشيفرة البرمجية السابقة سيتسبب بخطأ كما هو موضح:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon

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

تُشير الرسالة الأساسية إلى أن سبب الخطأ هو بسبب "أنواع غير متوافقة mismatched types". يدل تعريف الدالة plus_one على أنها تُعيد قيمةً من النوع i32 إلا أن التعبير لا يُقيَّم إلى قيمة، وهو الشيء المُعبّر بالقوسين () نوع الوحدة unit type، وبالتالي لا يوجد هناك أي قيمة لإعادتها مما يتناقض مع تعريف الدالة ويتسبب بخطأ. توفّر راست في رسالة الخطأ رسالةً لمساعدتك في حل هذه المشكلة إذ تقترح إزالة الفاصلة المنقوطة مما سيحلّ المشكلة بدوره.

التعليقات

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

إليك تعليقًا بسيطًا:

// hello, world

يبدأ التعليق في لغة راست بشرطتين مائلتين ويستمر التعليق إلى نهاية السطر، وإذا أردت استخدام التعليق ليشمل عدّة أسطر فعليك استخدام // في كل سطر كما يلي:

// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

يُمكن إضافة التعليقات في نهاية الأسطر البرمجية:

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

fn main() {
    let lucky_number = 7; // I’m feeling lucky today
}

إلا أنك غالبًا ما سترى التعليقات بالتنسيق التالي على سطر منفصل عن بقية الشيفرة البرمجية التي تشرحها:

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

fn main() {
    // I’m feeling lucky today
    let lucky_number = 7;
}

يوجد طريقة أخرى لكتابة التعليقات ألا وهي التعليقات التوثيقية documentation comments التي سنناقشها لاحقًا.

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


×
×
  • أضف...