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

الدوال (Functions)

تنتشر الدوال في كود Rust. لقد رأيت بالفعل واحدة من أهم الدوال في اللغة: دالة main ، وهي نقطة الدخول (entry point) للعديد من البرامج. لقد رأيت أيضاً الكلمة المفتاحية fn ، والتي تسمح لك بالتصريح عن دوال جديدة.

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

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

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

    another_function();
}

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

نقوم بتعريف دالة في Rust عن طريق إدخال fn متبوعة باسم الدالة ومجموعة من الأقواس. تخبر الأقواس المتعرجة (curly brackets) المصرف (compiler) بمكان بداية ونهاية جسم الدالة (function body).

يمكننا استدعاء أي دالة قمنا بتعريفها عن طريق إدخال اسمها متبوعاً بمجموعة من الأقواس. نظراً لأن another_function معرفة في البرنامج، يمكن استدعاؤها من داخل دالة main. لاحظ أننا قمنا بتعريف another_function بعد دالة main في الكود المصدري؛ كان بإمكاننا تعريفها قبل ذلك أيضاً. لا يهتم Rust بمكان تعريف دوالك، طالما أنها معرفة في مكان ما في نطاق (scope) يمكن رؤيته من قبل المستدعي (caller).

دعونا نبدأ مشروعاً ثنائياً (binary project) جديداً باسم functions لاستكشاف الدوال بشكل أكبر. ضع مثال another_function في ملف src/main.rs وقم بتشغيله. يجب أن ترى المخرجات التالية:

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

يتم تنفيذ الأسطر بالترتيب الذي تظهر به في دالة main. أولاً يتم طباعة رسالة “Hello, world!” ، ثم يتم استدعاء another_function وطباعة رسالتها.

المعلمات (Parameters)

يمكننا تعريف الدوال بحيث تحتوي على معلمات (parameters)، وهي متغيرات خاصة تشكل جزءاً من توقيع الدالة (function’s signature). عندما تحتوي الدالة على parameters، يمكنك تزويدها بقيم ملموسة (concrete values) لتلك المعلمات. تقنياً، تسمى القيم الملموسة وسائط (arguments)، ولكن في المحادثات العادية، يميل الناس إلى استخدام الكلمتين parameter و argument بالتبادل سواء للمتغيرات في تعريف الدالة أو للقيم الملموسة التي يتم تمريرها عند استدعاء الدالة.

في هذا الإصدار من another_function نضيف parameter:

اسم الملف: 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` profile [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

يحتوي التصريح عن another_function على parameter واحد باسم x. تم تحديد نوع x على أنه i32. عندما نمرر 5 إلى another_function ، يضع ماكرو println! القيمة 5 مكان زوج الأقواس المتعرجة الذي يحتوي على x في سلسلة التنسيق (format string).

في توقيعات الدوال، يجب عليك التصريح عن نوع كل parameter. هذا قرار متعمد في تصميم Rust: إن اشتراط توضيحات النوع (type annotations) في تعريفات الدوال يعني أن compiler لا يحتاج منك تقريباً لاستخدامها في أي مكان آخر في الكود لمعرفة النوع الذي تقصده. كما يستطيع compiler تقديم رسائل خطأ أكثر فائدة إذا كان يعرف الأنواع التي تتوقعها الدالة.

عند تعريف عدة parameters، افصل بين التصريحات عن المعلمات بفاصلة، هكذا:

اسم الملف: 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_measurement مع اثنين من parameters. المعلمة الأولى تسمى value وهي من نوع i32. والثانية تسمى unit_label وهي من نوع char. تقوم الدالة بعد ذلك بطباعة نص يحتوي على كل من value و unit_label.

دعونا نجرب تشغيل هذا الكود. استبدل البرنامج الموجود حالياً في ملف src/main.rs بمشروع functions الخاص بك بالمثال السابق وقم بتشغيله باستخدام cargo run:

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

لأننا استدعينا الدالة بالقيمة 5 كقيمة لـ value و 'h' كقيمة لـ unit_label ، فإن مخرجات البرنامج تحتوي على تلك القيم.

الجمل والتعبيرات (Statements and Expressions)

تتكون أجسام الدوال من سلسلة من الجمل (statements) التي تنتهي اختيارياً بتعبير (expression). حتى الآن، لم تتضمن الدوال التي غطيناها تعبيراً ختامياً، لكنك رأيت تعبيراً كجزء من statement. نظراً لأن Rust لغة قائمة على التعبيرات (expression-based language)، فهذا تمييز مهم يجب فهمه. اللغات الأخرى ليس لديها نفس التمييزات، لذا دعونا نلقي نظرة على ماهية statements و expressions وكيف تؤثر اختلافاتهم على أجسام الدوال.

  • الجمل (Statements) هي تعليمات تؤدي بعض الإجراءات ولا تعيد قيمة.
  • التعبيرات (Expressions) تؤول إلى قيمة ناتجة.

دعونا نلقي نظرة على بعض الأمثلة.

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

fn main() {
    let y = 6;
}

تعريفات الدوال هي أيضاً statements؛ المثال السابق بأكمله هو statement في حد ذاته. (ومع ذلك، كما سنرى قريباً، فإن استدعاء الدالة ليس statement).

لا تعيد Statements قيماً. لذلك، لا يمكنك تعيين let statement لمتغير آخر، كما يحاول الكود التالي القيام به؛ ستحصل على خطأ:

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

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

عند تشغيل هذا البرنامج، فإن الخطأ الذي ستحصل عليه سيبدو كالتالي:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

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

warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted

جملة let y = 6 لا تعيد قيمة، لذا لا يوجد شيء ليرتبط به x. هذا يختلف عما يحدث في لغات أخرى، مثل C و Ruby، حيث تعيد عملية التعيين (assignment) قيمة التعيين. في تلك اللغات، يمكنك كتابة x = y = 6 ويكون لكل من x و y القيمة 6 ؛ ليس هذا هو الحال في Rust.

تؤول Expressions إلى قيمة وتشكل معظم بقية الكود الذي ستكتبه في Rust. فكر في عملية حسابية، مثل 5 + 6 ، وهي expression يؤول إلى القيمة 11. يمكن أن تكون Expressions جزءاً من statements: في القائمة 3-1، القيمة 6 في الجملة let y = 6; هي expression يؤول إلى القيمة 6. استدعاء الدالة هو expression. استدعاء الماكرو هو expression. كتلة النطاق (scope block) الجديدة التي يتم إنشاؤها باستخدام الأقواس المتعرجة هي expression، على سبيل المثال:

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

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

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

هذا التعبير:

{
    let x = 3;
    x + 1
}

هو كتلة (block) تؤول في هذه الحالة إلى 4. ترتبط هذه القيمة بـ y كجزء من let statement. لاحظ سطر x + 1 بدون فاصلة منقوطة (semicolon) في نهايته، وهو ما يختلف عن معظم الأسطر التي رأيتها حتى الآن. لا تتضمن Expressions فواصل منقوطة ختامية. إذا أضفت فاصلة منقوطة إلى نهاية expression، فإنك تحوله إلى statement، وعندها لن يعيد قيمة. ضع ذلك في الاعتبار بينما تستكشف قيم إرجاع الدوال والتعبيرات تالياً.

دوال ذات قيم إرجاع (Functions with Return Values)

يمكن للدوال إعادة قيم إلى الكود الذي يستدعيها. نحن لا نسمي قيم الإرجاع (return values)، ولكن يجب علينا التصريح عن نوعها بعد سهم (->). في Rust، قيمة إرجاع الدالة مرادفة لقيمة التعبير الأخير في كتلة جسم الدالة. يمكنك الإرجاع مبكراً من دالة باستخدام الكلمة المفتاحية return وتحديد قيمة، ولكن معظم الدوال تعيد التعبير الأخير بشكل ضمني (implicitly). إليك مثال لدالة تعيد قيمة:

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

fn five() -> i32 {
    5
}

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

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

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

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

الرقم 5 في five هو قيمة إرجاع الدالة، ولهذا السبب نوع الإرجاع هو i32. دعونا نفحص هذا بمزيد من التفصيل. هناك جزئيتان مهمتان: أولاً، يظهر السطر let x = five(); أننا نستخدم قيمة إرجاع الدالة لتهيئة متغير. نظراً لأن الدالة five تعيد 5 ، فإن هذا السطر هو نفسه السطر التالي:

#![allow(unused)]
fn main() {
let x = 5;
}

ثانياً، دالة five ليس لها parameters وتحدد نوع قيمة الإرجاع، لكن جسم الدالة هو 5 وحيد بدون فاصلة منقوطة لأنه expression نريد إعادة قيمته.

دعونا نلقي نظرة على مثال آخر:

اسم الملف: 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 ، محولين إياه من expression إلى statement؟

اسم الملف: 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 to return this value

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

رسالة الخطأ الرئيسية، mismatched types ، تكشف عن المشكلة الجوهرية في هذا الكود. يقول تعريف الدالة plus_one أنها ستعيد i32 ، لكن statements لا تؤول إلى قيمة، وهو ما يتم التعبير عنه بـ () ، النوع الوحدوي (unit type). لذلك، لا يتم إرجاع أي شيء، مما يتعارض مع تعريف الدالة ويؤدي إلى حدوث خطأ. في هذه المخرجات، يقدم Rust رسالة للمساعدة في تصحيح هذه المشكلة: يقترح إزالة الفاصلة المنقوطة، مما سيصلح الخطأ.