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

تدفق التحكم (Control Flow)

تعد القدرة على تشغيل بعض الأكواد بناءً على ما إذا كان الشرط true والقدرة على تشغيل بعض الأكواد بشكل متكرر بينما يكون الشرط true من اللبنات الأساسية في معظم لغات البرمجة. أكثر البنيات شيوعاً التي تتيح لك التحكم في تدفق تنفيذ كود Rust هي تعبيرات if والحلقات (loops).

تعبيرات if (if Expressions)

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

أنشئ مشروعاً جديداً يسمى branches في دليل projects الخاص بك لاستكشاف تعبير if. في ملف src/main.rs ، أدخل ما يلي:

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

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

تبدأ جميع تعبيرات if بالكلمة المفتاحية if ، متبوعة بشرط. في هذه الحالة، يتحقق الشرط مما إذا كان المتغير number يحتوي على قيمة أقل من 5 أم لا. نضع كتلة الكود المراد تنفيذها إذا كان الشرط true مباشرة بعد الشرط داخل أقواس متعرجة. تسمى كتل الكود المرتبطة بالشروط في تعبيرات if أحياناً بالأذرع (arms)، تماماً مثل arms في تعبيرات match التي ناقشناها في قسم “مقارنة التخمين بالرقم السري” من الفصل الثاني.

اختيارياً، يمكننا أيضاً تضمين تعبير else ، وهو ما اخترنا القيام به هنا، لمنح البرنامج كتلة كود بديلة لتنفيذها في حالة تقييم الشرط إلى false. إذا لم تقدم تعبير else وكان الشرط false ، فسيقوم البرنامج ببساطة بتخطي كتلة if والانتقال إلى الجزء التالي من الكود.

حاول تشغيل هذا الكود؛ يجب أن ترى المخرجات التالية:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

دعونا نحاول تغيير قيمة number إلى قيمة تجعل الشرط false لنرى ما سيحدث:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

قم بتشغيل البرنامج مرة أخرى، وانظر إلى المخرجات:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

من الجدير بالذكر أيضاً أن الشرط في هذا الكود يجب أن يكون من النوع المنطقي (bool). إذا لم يكن الشرط bool ، فسنحصل على خطأ. على سبيل المثال، حاول تشغيل الكود التالي:

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

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

يتم تقييم شرط if إلى قيمة 3 هذه المرة، ويقوم Rust بإصدار خطأ:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

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

يشير الخطأ إلى أن Rust توقع bool ولكنه حصل على عدد صحيح (integer). على عكس لغات مثل Ruby و JavaScript، لن يحاول Rust تلقائياً تحويل الأنواع غير المنطقية إلى Boolean. يجب أن تكون صريحاً وتزود if دائماً بقيمة Boolean كشرط لها. إذا أردنا تشغيل كتلة كود if فقط عندما لا يساوي الرقم 0 ، على سبيل المثال، يمكننا تغيير تعبير if إلى ما يلي:

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

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

سيؤدي تشغيل هذا الكود إلى طباعة number was something other than zero.

التعامل مع شروط متعددة باستخدام else if

يمكنك استخدام شروط متعددة من خلال الجمع بين if و else في تعبير else if. على سبيل المثال:

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

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

يحتوي هذا البرنامج على أربعة مسارات محتملة يمكنه اتخاذها. بعد تشغيله، يجب أن ترى المخرجات التالية:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

عندما يتم تنفيذ هذا البرنامج، فإنه يتحقق من كل تعبير if بالترتيب وينفذ أول جسم يتم تقييم شرطه إلى true. لاحظ أنه على الرغم من أن 6 يقبل القسمة على 2، إلا أننا لا نرى المخرجات number is divisible by 2 ، ولا نرى نص number is not divisible by 4, 3, or 2 من كتلة else. وذلك لأن Rust ينفذ فقط الكتلة الخاصة بأول شرط true ، وبمجرد العثور على واحد، فإنه لا يتحقق حتى من الباقي.

يمكن أن يؤدي استخدام الكثير من تعبيرات else if إلى جعل الكود الخاص بك مزدحماً، لذا إذا كان لديك أكثر من واحد، فقد ترغب في إعادة هيكلة (refactor) الكود الخاص بك. يصف الفصل السادس بنية تفريع قوية في Rust تسمى match لهذه الحالات.

استخدام if في عبارة let

نظراً لأن if هو تعبير (expression)، يمكننا استخدامه على الجانب الأيمن من عبارة let لتعيين النتيجة لمتغير، كما في القائمة 3-2.

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

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

سيتم ربط المتغير number بقيمة بناءً على نتيجة تعبير if. قم بتشغيل هذا الكود لترى ما سيحدث:

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

تذكر أن كتل الكود يتم تقييمها إلى آخر تعبير فيها، والأرقام بحد ذاتها هي أيضاً تعبيرات. في هذه الحالة، تعتمد قيمة تعبير if بالكامل على كتلة الكود التي يتم تنفيذها. هذا يعني أن القيم التي لديها القدرة على أن تكون نتائج من كل ذراع من أذرع if يجب أن تكون من نفس النوع؛ في القائمة 3-2، كانت نتائج كل من ذراع if وذراع else أعداداً صحيحة من نوع i32. إذا كانت الأنواع غير متطابقة، كما في المثال التالي، فسنحصل على خطأ:

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

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

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

عندما نحاول تصريف (compile) هذا الكود، سنحصل على خطأ. أذرع if و else لها أنواع قيم غير متوافقة، ويشير Rust بالضبط إلى مكان العثور على المشكلة في البرنامج:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

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

يتم تقييم التعبير في كتلة if إلى عدد صحيح، ويتم تقييم التعبير في كتلة else إلى سلسلة نصية (string). لن يعمل هذا، لأن المتغيرات يجب أن يكون لها نوع واحد، ويحتاج Rust إلى معرفة نوع المتغير number بشكل قاطع في وقت التصريف (compile time). تتيح معرفة نوع number للمصرف (compiler) التحقق من أن النوع صالح في كل مكان نستخدم فيه number. لن يتمكن Rust من القيام بذلك إذا تم تحديد نوع number فقط في وقت التشغيل (runtime)؛ سيكون المصرف أكثر تعقيداً وسيقدم ضمانات أقل حول الكود إذا كان عليه تتبع أنواع افتراضية متعددة لأي متغير.

التكرار باستخدام الحلقات (Repetition with Loops)

غالباً ما يكون من المفيد تنفيذ كتلة من الكود أكثر من مرة. لهذه المهمة، يوفر Rust عدة حلقات (loops)، والتي ستعمل من خلال الكود الموجود داخل جسم الحلقة حتى النهاية ثم تبدأ فوراً من البداية. لتجربة الحلقات، دعونا ننشئ مشروعاً جديداً يسمى loops.

يحتوي Rust على ثلاثة أنواع من الحلقات: loop و while و for. دعونا نجرب كل واحدة منها.

تكرار الكود باستخدام loop

تخبر الكلمة المفتاحية loop لغة Rust بتنفيذ كتلة من الكود مراراً وتكراراً إما إلى الأبد أو حتى تخبرها صراحة بالتوقف.

كمثال، قم بتغيير ملف src/main.rs في دليل loops الخاص بك ليبدو كالتالي:

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

fn main() {
    loop {
        println!("again!");
    }
}

عندما نشغل هذا البرنامج، سنرى كلمة again! تُطبع مراراً وتكراراً بشكل مستمر حتى نوقف البرنامج يدوياً. تدعم معظم واجهات الأوامر (terminals) اختصار لوحة المفاتيح ctrl-C لمقاطعة برنامج عالق في حلقة مستمرة. جرب ذلك:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

يمثل الرمز ^C المكان الذي ضغطت فيه على ctrl-C.

قد ترى أو لا ترى كلمة again! مطبوعة بعد ^C ، اعتماداً على مكان الكود في الحلقة عندما تلقى إشارة المقاطعة.

لحسن الحظ، يوفر Rust أيضاً طريقة للخروج من الحلقة باستخدام الكود. يمكنك وضع الكلمة المفتاحية break داخل الحلقة لإخبار البرنامج متى يتوقف عن تنفيذ الحلقة. تذكر أننا فعلنا ذلك في لعبة التخمين في قسم “الخروج بعد التخمين الصحيح” من الفصل الثاني للخروج من البرنامج عندما فاز المستخدم باللعبة من خلال تخمين الرقم الصحيح.

استخدمنا أيضاً continue في لعبة التخمين، والتي تخبر البرنامج في الحلقة بتخطي أي كود متبقٍ في هذه الدورة (iteration) من الحلقة والانتقال إلى الدورة التالية.

إرجاع القيم من الحلقات

أحد استخدامات loop هو إعادة محاولة عملية تعرف أنها قد تفشل، مثل التحقق مما إذا كان الخيط (thread) قد أكمل مهمته. قد تحتاج أيضاً إلى تمرير نتيجة تلك العملية خارج الحلقة إلى بقية الكود الخاص بك. للقيام بذلك، يمكنك إضافة القيمة التي تريد إرجاعها بعد تعبير break الذي تستخدمه لإيقاف الحلقة؛ سيتم إرجاع تلك القيمة خارج الحلقة حتى تتمكن من استخدامها، كما هو موضح هنا:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

قبل الحلقة، نعلن عن متغير باسم counter ونقوم بتهيئته بالقيمة 0. ثم نعلن عن متغير باسم result للاحتفاظ بالقيمة المرجعة من الحلقة. في كل دورة من دورات الحلقة، نضيف 1 إلى المتغير counter ، ثم نتحقق مما إذا كان counter يساوي 10. عندما يكون كذلك، نستخدم الكلمة المفتاحية break مع القيمة counter * 2. بعد الحلقة، نستخدم فاصلة منقوطة لإنهاء العبارة التي تعين القيمة لـ result. أخيراً، نطبع القيمة في result ، وهي في هذه الحالة 20.

يمكنك أيضاً استخدام return من داخل الحلقة. بينما يخرج break فقط من الحلقة الحالية، فإن return يخرج دائماً من الدالة الحالية.

إزالة الغموض باستخدام تسميات الحلقات (Loop Labels)

إذا كان لديك حلقات داخل حلقات، فإن break و continue ينطبقان على الحلقة الداخلية في تلك النقطة. يمكنك اختيارياً تحديد تسمية للحلقة (loop label) على حلقة يمكنك استخدامها بعد ذلك مع break أو continue لتحديد أن تلك الكلمات المفتاحية تنطبق على الحلقة المسماة بدلاً من الحلقة الداخلية. يجب أن تبدأ تسميات الحلقات بفاصلة علوية واحدة. إليك مثال مع حلقتين متداخلتين:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

تقوم الحلقة الداخلية بدون تسمية بالعد التنازلي من 10 إلى 9. أول break لا يحدد تسمية سيخرج من الحلقة الداخلية فقط. أما عبارة break 'counting_up; فستخرج من الحلقة الخارجية. يطبع هذا الكود:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

تبسيط الحلقات الشرطية باستخدام while

غالباً ما يحتاج البرنامج إلى تقييم شرط داخل حلقة. طالما أن الشرط true ، تعمل الحلقة. عندما يتوقف الشرط عن كونه true ، يستدعي البرنامج break ، مما يوقف الحلقة. من الممكن تنفيذ سلوك مثل هذا باستخدام مزيج من loop و if و else و break ؛ يمكنك تجربة ذلك الآن في برنامج، إذا أردت. ومع ذلك، فإن هذا النمط شائع جداً لدرجة أن Rust لديها بنية لغة مدمجة له، تسمى حلقة while. في القائمة 3-3، نستخدم while لتدوير البرنامج ثلاث مرات، والعد التنازلي في كل مرة، ثم بعد الحلقة، لطباعة رسالة والخروج.

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

تزيل هذه البنية الكثير من التداخل (nesting) الذي سيكون ضرورياً إذا استخدمت loop و if و else و break ، وهي أكثر وضوحاً. طالما يتم تقييم الشرط إلى true ، يتم تشغيل الكود؛ وإلا، فإنه يخرج من الحلقة.

التكرار عبر مجموعة باستخدام for

يمكنك اختيار استخدام بنية while للتكرار عبر عناصر مجموعة (collection)، مثل المصفوفة (array). على سبيل المثال، تقوم الحلقة في القائمة 3-4 بطباعة كل عنصر في المصفوفة a.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

هنا، يقوم الكود بالعد التصاعدي عبر العناصر في المصفوفة. يبدأ من الفهرس (index) 0 ثم يدور حتى يصل إلى الفهرس النهائي في المصفوفة (أي عندما لا يعود index < 5 صحيحاً). سيؤدي تشغيل هذا الكود إلى طباعة كل عنصر في المصفوفة:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

تظهر جميع قيم المصفوفة الخمس في واجهة الأوامر، كما هو متوقع. على الرغم من أن index سيصل إلى قيمة 5 في مرحلة ما، إلا أن الحلقة تتوقف عن التنفيذ قبل محاولة جلب قيمة سادسة من المصفوفة.

ومع ذلك، فإن هذا النهج عرضة للأخطاء؛ فقد نتسبب في توقف البرنامج بشكل مفاجئ (panic) إذا كانت قيمة الفهرس أو شرط الاختبار غير صحيحين. على سبيل المثال، إذا قمت بتغيير تعريف المصفوفة a لتتكون من أربعة عناصر ولكنك نسيت تحديث الشرط إلى while index < 4 ، فسيحدث panic للكود. كما أنه بطيء، لأن المصرف يضيف كوداً في وقت التشغيل لإجراء الفحص الشرطي لما إذا كان الفهرس ضمن حدود المصفوفة في كل دورة عبر الحلقة.

كبديل أكثر إيجازاً، يمكنك استخدام حلقة for وتنفيذ بعض الأكواد لكل عنصر في المجموعة. تبدو حلقة for مثل الكود الموجود في القائمة 3-5.

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

عندما نشغل هذا الكود، سنرى نفس المخرجات كما في القائمة 3-4. والأهم من ذلك، أننا زدنا الآن من سلامة الكود وقضينا على فرصة حدوث أخطاء برمجية قد تنتج عن تجاوز نهاية المصفوفة أو عدم الذهاب بعيداً بما يكفي وفقدان بعض العناصر. يمكن أن يكون كود الآلة (machine code) الناتج عن حلقات for أكثر كفاءة أيضاً لأن الفهرس لا يحتاج إلى مقارنته بطول المصفوفة في كل دورة.

باستخدام حلقة for ، لن تحتاج إلى تذكر تغيير أي كود آخر إذا قمت بتغيير عدد القيم في المصفوفة، كما تفعل مع الطريقة المستخدمة في القائمة 3-4.

إن سلامة وإيجاز حلقات for تجعلها بنية الحلقات الأكثر استخداماً في Rust. حتى في المواقف التي تريد فيها تشغيل بعض الأكواد عدداً معيناً من المرات، كما في مثال العد التنازلي الذي استخدم حلقة while في القائمة 3-3، فإن معظم مبرمجي رست (Rustaceans) سيستخدمون حلقة for. الطريقة للقيام بذلك هي استخدام النطاق (Range)، الذي توفره المكتبة القياسية، والذي يولد جميع الأرقام بالتسلسل بدءاً من رقم واحد وانتهاءً قبل رقم آخر.

إليك كيف سيبدو العد التنازلي باستخدام حلقة for وطريقة أخرى لم نتحدث عنها بعد، وهي rev ، لعكس النطاق:

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

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

هذا الكود أجمل قليلاً، أليس كذلك؟

ملخص (Summary)

لقد فعلتها! كان هذا فصلاً كبيراً: لقد تعلمت عن المتغيرات، وأنواع البيانات البسيطة والمركبة، والدوال، والتعليقات، وتعبيرات if ، والحلقات! للتدرب على المفاهيم التي تمت مناقشتها في هذا الفصل، حاول بناء برامج للقيام بما يلي:

  • تحويل درجات الحرارة بين فهرنهايت وسيلسيوس.
  • توليد رقم فيبوناتشي ذو الترتيب n.
  • طباعة كلمات أغنية عيد الميلاد “The Twelve Days of Christmas” ، مع الاستفادة من التكرار في الأغنية.

عندما تكون مستعداً للمضي قدماً، سنتحدث عن مفهوم في Rust لا يوجد عادةً في لغات البرمجة الأخرى: الملكية (ownership).