Rust/לולאות/מתחילים

Rust

בשיעור זה נלמד על לולאות שהן כלי בסיסי המשמש לחזרה על קטע קוד מספר מסוים של פעמים בתוכנית. הלולאות חוסכות זמן רב בעת כתיבת התוכנה ומסדרות את הקוד. רוב הלולאות בהן נשתמש בתוכנית שלנו יפעלו עד למימושו של תנאי מסוים.

הצורך בלולאות עריכה

יתכן שבמהלך תוכנית שנכתוב נרצה שהמחשב יבצע את אותה הפעולה מספר פעמים. למשל, יתכן שנרצה שהמחשב ידפיס למשתמש את המחרוזת "שלום עולם" במשך 5 פעמים. כיצד נגרום לו לעשות זאת? ניתן לשכפל את פקודת ההדפסה במשך 5 פעמים, כמו בדוגמה הבאה:

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

    println!("Hello, world!\n");

    println!("Hello, world!\n");

    println!("Hello, world!\n");

    println!("Hello, world!\n");
}

פתרון זה הוא פתרון מאוד מעניין, אבל יש בו המון חסרונות. מה אם למשל היינו רוצים לשכפל את הקוד לא 5 פעמים אלא 100 פעמים? האם היינו עוברים ומעתיקים הפקודה פעם אחר פעם במשך 100 פעמים?אולי חלק יאמרו ששכפול הקוד 100 פעמים אינו מוגזם, אבל מה אם נרצה להדפיס את ההודעה 1000 פעמים או 10000 פעמים?

כמו כן, כיצד נוודא שאכן הצלחנו לשכפל את הקוד 100 פעמים בדיוק ושלא פספסנו פעם אחת? ואם עשינו טעות ואנחנו רוצים לתקן משהו בפקודה? אנחנו נעבור על כל השכפולים אחד אחרי השני ונתקן זאת באופן ידני? מה גם ששכפול הקוד במשך 100 פעמים לוקח המון זמן והופך את הקוד נורא מסורבל. ואם אנחנו בכלל רוצים שכמות הפעמים תהיה תלויה במספר שקיבלנו במהלך התוכנית, איך נעשה זאת אז?

קל לראות שיש המון בעיות שעלולות לצוץ בשיטה זו. לשם כך, הומצאו הלולאות, המאפשרות לנו לחזור על קטע קוד מספר מסוים של פעמים תוך שמירה על סדר הקוד ומניעת הבעיות שהוזכרו לעיל.

לולאת loop עריכה

הלולאה הפשוטה ביותר בשפת ראסט היא לולאת loop שפועלת אין סוף פעמים. כמו שכבר ראינו במקומות אחרים, גם רצף הפעולות של הלולאה מוגדר באמצעות בלוקים. תחילה תבוא המילית loop שמסמלת לקומפיילר שבחרנו להשתמש בלולאת loop ואחריה יבוא רצף פעולות מסויים, עליו תחזור הלולאה במשך אין ספור פעמים. כל ריצה או חזרה של הלולאה על רצף הפעולות בתוך הבלוק מכונה "איטרציה".

loop {
    println!("Hello, world!\n");
}

println!("The computer will never print this line!");

מרגע היכנסו ללולאה, המחשב יבצע פקודה אחר פקודה בלולאה, לפי הסדר בו הפקודות נכתבו בקוד וכשיגיע לבלוק הסוגר, יחזור לתחילת הלולאה ויתחיל לבצע את אותן הפעולות שוב ולפי אותו הסדר. מכיוון שהלולאה loop היא לולאה אינסופית, המחשב לעולם לא יצא מן הלולאה ולכן קטע קוד שייכתב אחרי הלולאה לעולם לא יתבצע. בדוגמה האחרונה, המחשב ידפיס את המחרוזת "Hello, world!" אינסוף פעמים ולכן לעולם לא יגיע לביטוי שאחרי הבלוק הסוגר של הלולאה ולעולם לא ידפיס אותו.

הוראת break לשבירת הלולאה עריכה

אבל אם אנחנו רוצים להדפיס למשתמש רק 5 פעמים את המחרוזת "Hello, world!", כיצד נעשה זאת? הרי ראינו שהלולאה הקודמת רצה במשך אינספור פעמים. לשם כך, הומצאה הוראת "break" ש"שוברת" את הלולאה. בעצם הוראה זו, נותנת למחשב פקודה להפסיק לבצע את הפעולות שבתוך הלולאה ולחזור לבצע את הפעולות שאחרי בלוק הסיום של הלולאה. נסתכל למשל בדוגמת הקוד הבאה:

loop {
    println!("Hello, world!\n");
    break;
    println!("The computer will never print this line!");
}

println!("The computer will print this line!");

בדוגמה האחרונה, המחשב ידפיס פעם אחת את המחרוזת "Hello, world!". אחרי כן, כשייתקל בהוראת break, הוא ייצא מן הלולאה ויחזור לרצף הפקודות שמופיע אחרי הבלוק הסוגר. יציאתו של המחשב מן הלולאה גורמת לכך שמעתה ואילך אף לא פקודה אחת שנכתבה בתוך הלולאה תתבצע, גם הפקודות שעדיין לא התבצעו אפילו פעם אחת. לכן, המחשב לעולם לא יגיע לפקודה שנמצאת אחרי ה-break ולעולם לא תודפס שורה זו. לעומת זאת, בשל שבירת הלולאה, השורה שנמצאת אחרי הבלוק הסוגר של הלולאה אכן תתבצע ולכן ההודעה שם אכן תודפס.


אז איך נוכל להדפיס את המחרוזת "Hello, world!" במשך 5 פעמים בלבד?

כדי לעשות זאת נוסיף ללולאה תנאי שיבדוק את מספר האיטרציות של הלולאה. לשם כך, ניצור משתנה, אותו נעלה בכל איטרציה של הלולאה באחד. אחרי כן, נבדוק בעזרת תנאי פשוט האם ערכו של המשתנה כבר הגיע ל-5 ואם התוצאה תהיה אמת, נשבור את הלולאה ונצא ממנה.

let mut i = 0;
loop {
    println!("Hello, world!\n");

    i += 1;
    if (i == 5)
    {
    	break;
    }
}


 

עכשיו תורכם:

נסו בעזרת לולאת loop והוראת break להדפיס את כל המספרים הטבעיים מ-1 עד 10. ודאו שאינכם מדפיסים מספרים הגדולים מעשר. רמז: צרו משתנה אותו תעלו בכל איטרציה של הלולאה באחד והדפיסו את תוכנו.

לולאת while עריכה

סוג נוסף של לולאות בשפת ראסט הן לולאות while. בשונה מלולאות loop שירוצו במשך אינספור פעמים, לולאות while יפעלו כל עוד תנאי מסוים יתממש, כלומר כל עוד תנאי מסוים יחזיר לנו תוצאת אמת (true). אם התנאי לא יתממש, המחשב לא יבצע את רצף הפעולות של הלולאה.

המבנה של לולאת while דומה לזה של תנאי פשוט. תחילה תבוא המילית while ואחריה יבוא תמיד ביטוי בוליאני כלשהו, שתוצאתו יכולה להיות אמת או שקר. אחרי הביטוי יבוא בלוק שיגדיר את רצף הפעולות שיבוצע אם התנאי החזיר תוצאת אמת. נתסכל למשל על הדוגמה הבאה:

let mut x = 1;
while x <= 10 {
    println!("{}\n", x);
    x+=1;
}

בעת שמחשב יתקל בלולאת while, הוא יבדוק את התנאי שעומד בבסיסה. במקרה שלנו המחשב יבדוק האם x קטן שווה ל-10. מאחר שתוצאת ביטוי זה היא אמת, ייכנס המחשב ללולאה ויבצע את רצף הפעולות שבתוכה. בסיום כל איטרציה של הלולאה, יבדוק המחשב את התנאי מחדש. אם התוצאה שלו תהיה עדיין אמת, יבצע שוב את רצף הפעולות של הלולאה. בדוגמה שלמעלה גרמנו למחשב להדפיס את כל המספרים הטבעיים בין 1- ל-10.

המחשב ייכנס ללולאה אך ורק אם התנאי שעומד בבסיסה יתממש. בדוגמה הבאה, המחשב לא ידפיס כלום, מכיוון שהתנאי שעומד בבסיס הלולאה יחזיר תוצאה שקר ולכן המחשב בכלל לא ייכנס ללולאה.

// print all the negative numbers from x to 0
let mut x = 1;
while x <= 0 {
    println!("{}\n", x);
    x+=1;
}
 

כדאי לדעת:

למרות שאת כל מה שנרצה לעשות עם לולאת while ניתן גם לעשות עם לולאת loop והוראת break, השימוש בלולאת while הרבה יותר נוח ורווח בקרב מתכנתים. קוד המשתמש בלולאת loop שלא בשביל פעולה אינסופית נחשב לקוד מסורבל ולכן, כשנרצה לכתוב קוד המשלב בתוכו לולאה שאינה אינסופית, נעדיף להשתמש בלולאת while.

לולאת for עריכה

סוג נוסף של לולאות השונה מן הסוגים האחרים שהכרנו קודם לכן, הוא לולאות for, המשמשות לביצוע של מבחר פעולות בשפת ראסט. בעזרת לולאות אלו ניתן לבצע המון דברים מורכבים שבחלקם נוכל לגעת רק בשיעורים הבאים. בשיעור זה נלמד על המבנה הכללי של לולאות for ואיך לבצע בעזרתן פעולות בסיסיות על מספרים.

המבנה של לולאות for שונה מזה של לולאות loop ו-while שנתקלו בהן עד עתה. תחילה תבוא המילית for שמציינת את סוג הלולאה ואחריה יבוא משתנה לבחירתו של המשתמש. אחרי המשתנה, תבוא תמיד המילית "in" ואחריה יבוא טווח מסוים של ערכים (משתנים). בדוגמאות הבאות נעסוק בטווח של מספרים. אחרי כן, יבוא הבלוק שיגדיר את רצף הפעולות של הלולאה. נסתכל על המבנה הבסיסי של לולאות for בדוגמה הבאה:

for <variable> in <range> {
	<action>
}

טווח הערכים של לולאות for עריכה

טווח הערכים של הלולאה הוא למעשה טווח הערכים עליו הלולאה תרוץ. גודלו של טווח זה הוא הדבר שקובע את מספר האיטרציות של הלולאה - כגודל טווח הערכים כך יהיה מספר האיטרציות. למשל, אם נרצה להדפיס את כל המספרים בין 1 ל-100, נאמר שטווח הערכים של הלולאה הוא בין 1 ל-100 (כולל 100) ושמספר האיטרציות של הלולאה יהיה גם הוא 100 (בין 1 ל-100 יש 100 מספרים טבעיים).

כדי למנוע בלבול בהחלטה לגבי מהו הקצה של הטווח (האם הוא כולל את המספר האחרון שנמצא בקצהו או לא), הוחלט שהאיבר שיבוא אחרון (בקצה) לא ייכלל בטווח. לכן, אם רוצים להדפיס את כל המספרים בין 1- ל-100, נאמר שהטווח הוא לא 1-100 אלא 1-101. שימו לב שמספר האיטרציות של הלולאה לא השתנה - עבור הטווח 1-101 (לא כולל המספר 101), ישנם 100 מספרים טבעיים ולכן מספר האיטרציות של הלולאה יהיה 100.

בשפת ראסט, טווח של ערכים מסומן על ידי שתי נקודות (' .. ').

for x in 1..101 {
	<action>
}

בדוגמה מעל, הלולאה תחלוף על כל המספרים בטווח שבין 1-101, לא כולל המספר 101. לכן, אם נבקש מהלולאה להדפיס לנו את כל המספרים שבטווח זה, היא לא תדפיס לנו את המספר 101.

אם בכל זאת נרצה שהלולאה תתייחס גם למספר 101, נוכל להוסיף '=' בסוף הטווח, כך שנקבל (' =.. ') ובזאת לסמן לקומפיילר להתייחס גם לקצה הנגדי של הטווח. לדוגמה:

for x in 1..=101 {
	<action>
}

המשתנה העומד בבסיס הלולאה עריכה

כפי שאמרנו מקודם, אחרי המילית for תמיד יבוא משתנה. עד עתה לא עסקנו בשאלה מה הוא המשתנה הזה, אבל בדוגמה שמעלנו ניתן לזהות את המשתנה x כזה (למרות שאנחנו לא עושים בו כל שימוש). המשתנה שבא אחרי המילית for מכונה בשם "המשתנה העומד בבסיס הלולאה". זהו משתנה ייחודי שערכו משתנה ומתעדכן בכל איטרציה של הלולאה.

המשתנה העומד בבסיס הלולאה יכיל בכל איטרציה ערך מתוך טווח הערכים של הלולאה. במקרה שלנו הערך הזה יהיה מספרי. באיטרציה הבאה של הלולאה, המשתנה יכיל את הערך הבא הנמצא בטווח - במקרה שלנו, המספר העוקב למספר הקודם. למעשה, לולאות for פועלות כך שבאיטרציה הראשונה של הלולאה, יכיל המשתנה את האיבר שנמצא במקום הראשון בטווח הערכים של הלולאה. באיטרציה השנייה של הלולאה, את האיבר שנמצא במקום השני, וכן הלאה. נסתכל למשל בדוגמה הבאה:

for x in 1..101 {
	println!("{}\n", x);
}

בדוגמה שלפנינו יש לולאת for המדפיסה את כל המספרים בין 1-ל-100. טווח הלולאה הוא בין המספרים 1-101 (לא כולל 101) והלולאה חולפת מספר אחרי מספר ומדפיסה אותו. באיטרציה הראשונה של הלולאה, המספר הראשון שיודפס יהיה 1, משום שהמספר 1 הוא המספר הראשון בטווח הערכים של הלולאה לכן ערכו של x יהיה אחד. באיטרציה השנייה יודפס המספר 2, בשלישית 3, וכן הלאה.

שימו לב לא להתבלבל! המשתנה העומד בבסיס הלולאה לא מכיל את מספר האיטרציה הנוכחי של הלולאה, אלא את המשתנה המצוי במקום של המספר הנוכחי בטווח הערכים של הלולאה. אם הטווח המוגדר ללולאה הוא 5-10, אזי שבאיטרציה הראשונה של הלולאה המשתנה לא יכיל את המספר 1 אלא את המספר 5 (שכן הוא האיבר הראשון בטווח הלולאה).

אורך החיים של המשתנה העומד בבסיס הלולאה עריכה

כאמור, המשתנה העומד בבסיס הלולאה הוא משתנה מאוד ייחודי. אחת התכונות המיוחדות של משתנה זה היא שאין צורך להצהיר עליו כמו שאנחנו מצהירים על משתנה רגיל לפני הכניסה ללולאה. ההצהרה על המשתנה מתבצעת על ידי המילית for ולכן אין צורך להצהיר עליו כמו על משתנה רגיל באמצעות let. כשאנחנו מוסיפים לקוד לולאת for ובתוכה משתנה, אנחנו כמו מצהירים עליו בתוך רצף הפעולות של הלולאה:

{
	let x = num;
}

לכן, אורך החיים של המשתנה העומד בבסיס הלולאה מוגדר רק בתוך הבלוק של רצף הפעולות של הלולאה. ברגע שנצא מן הלולאה, לא נוכל להשתמש עוד בערכו של המשתנה:

for x in 5..10 {
	println!("{}\n", x);
}
println!("{}\n", x); // x is not found in this scope

כמו כל משתנה אחר, המשתנה העומד בבסיס הלולאה מוגדר כ-immutable ולכן, אם נרצה לבצע לו השמה, נצטרך להוסיף את המילית "mut" להצהרה על המשתנה אחרי ה-for. שימו לב שברוב המקרים נשאיר את המשתנה העומד בבסיס הלולאה בתור immutable מפני שסביר להניח שלא נצטרך לבצע לו פעולת השמה - ערכו של המשתנה משתנה בכל איטרציה של הלולאה ולא נשמר בסופה ולכן נדיר שנצטרך לאחסן בו ערך ולשמור אותו. למשל:

for mut x in 0..10 {
	x += 1;
	println!("{}\n", x); // we could just send (x + 1) instead of making x mutable
}

היפוך טווח המספרים בלולאה עריכה

נניח שאנחנו רוצים להדפיס את כל המספרים בין 50 ל-1 בסדר יורד, כלומר להדפיס את 50 ואז את 49, וכן הלאה, עד שנגיע בסוף ל-0. חלק מכם ודאי יחשבו שניתן לעשות זאת בעזרת היפוך של האיברים שעומדים בקצה הטווח - כמו שכתבנו "10..0", לכתוב "0..10". צר לנו לאכזב אתכם, אבל פתרון זה לא יעבוד.

הסיבה שהיפוך האיברים בקצוות לא עובד, היא ששפת ראסט מסתכלת על הטווח של לולאות for כעל מעין קיצור ללולאה רגילה שעומד בבסיסה תנאי ש"מוסתר" מן המשתמש. בעצם, המחשב מצפה מהמשתמש שהאיבר שיבוא ראשון (בקצה השמאלי) יהיה יותר קטן מן האיבר שיבוא אחרון (בקצה הימני) וזהו התנאי "המוסתר" של הלולאה. אם ננסה להריץ לולאת for עם איברים מהופכים, נראה שהקוד מתקמפל כרגיל אבל ששום דבר לא יודפס. הסיבה לכך היא שמבחינת המחשב, התנאי שעומד בבסיס הלולאה לא התממש ולכן הוא פועל כפי שהוא צריך ולא מדפיס דבר.

אז איך כן הופכים את סדר ההדפסה של המספרים בעזרת לולאת for?

ניתן להשתמש בפונקציית .rev() (קיצור של reverse - היפוך) שמבצעת היפוך לסדר הפעולה של הלולאה. פונקציה זו גורמת לכך שבאיטרציה הראשונה של הלולאה, הערך של המשתנה העומד בבסיס יהיה כערך האיבר האחרון הכלול בטווח, באיטרציה השנייה כערך האיבר הלפני-אחרון וכו'. כדי להשתמש בפונקציית rev(), יש לעטוף את טווח הלולאה בסוגריים ולהוסיף אחריהם את הפונקציה בעזרת נקודה:

for x in (1..=10).rev() {
	println!("{}\n", x);
}

לולאה מקוננת עריכה

לעתים אנו נצטרך להגדיר שתי לולאות במהלך התוכנית שלנו, כאשר אחת תהיה כלולה בתוך השנייה. למשל, אם נרצה לכתוב תוכנית שעוברת על טבלה המחולקת לטורים ושורות, נצטרך שתי לולאות - אחת שתעבור על השורות ואחת שתעבור על הטורים. לולאה שמוגדרת בתוך לולאה אחרת נקראת "לולאה מקוננת". נציג עתה דוגמה למקרה בו נזדקק ללולאה מקוננת:

fn main()
{
	let mut rows = 1;
	let mut columns = 1;
	
	while rows <= 10
	{	
		columns = 1;
		while columns <= 10
		{
			print!("{:4}", rows * columns);
			columns += 1;
		}
		println!();
		rows += 1 ;
	}
}


 

עכשיו תורכם:

הדפיסו את לוח הכפל בעזרת שתי לולאות for. שימו לב שבניגוד לדוגמה מעל אינכם זקוקים למשתנה מסוג mutable!

תרגול עריכה