Rust/פלט והצללה

Rust

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

פונקציית פלט עריכה

בשיעור הראשון השתמשנו בפונקציית המאקרו println!() כדי להדפיס למסך את הביטוי "Hello, world!". כזכור, הדפסת התווים נעשתה באופן הבא:

println!("Hello, world!");

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

פונקציית המאקרו print! עריכה

כזכור, פונקציית ה-println()! שבשמה מתחבא הביטוי "print line" היא פונקציית מאאקרו שמדפיסה שורת טקסט אחת למסך. משמעות הדבר, היא שבסיום כל הדפסה של ביטוי כלשהו בעזרת הפונקציה, יודפס באופן אוטומטי תו ה-Enter (שמסמל ירידת שורה). לכן, אם נקרא לפונקציה פעמיים ברצף, כל אחד מהביטויים שיישלח לפונקציה יודפס בשורה נפרדת. לדוגמה:

println!("This text will be printed");
println!("in a seperate line");

// output:
// This text will be printed
// in a seperate line

אם לא נרצה לגרום לירידת שורה אוטומטית בסיום ההדפסה, נוכל להשתמש בפונקציית המאקרו print!():

print!("This text will be printed");
print!(" in the same line");

// output:
// This text will be printed in the same line

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


 

כדאי לדעת:

אם במהלך הדפסת ביטוי מסוים תרצו לרדת שורה (כלומר, ללחוץ על Enter), מבלי לקרוא ל-println()! או להשתמש בה מספר פעמים, תוכלו להשתמש בתו המיוחד '\n' (בקסלאש אן). הוספת תו זה, תוביל לירידת שורה, כאשר כל הטקסט שייכתב אחריו יופיע כבר בשורה נפרדת. שימו לב שהרצף 'n\' לא יודפס למסך ביחד עם ירידת השורה וזאת משום מדובר בתו שהוגדר במיוחד כדי לסמל את ירידת שורה. אם תרצו להדפיס את הביטוי "n\" תצטרכו להוסיף לוכסן הפוך נוסף לפני הלוכסן הראשון, כלומר, "n\\". עוד על תווים מיוחדים וכיצד ניתן להדפיס אותם תוכלו לקרוא כאן (באנגלית).

הדפסת משתנים עריכה

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

let x = 5;
println!("the value of x is: {}", x);

// output:
// the value of x is 5

באותו האופן, ניתן לשלוח בו זמנית מספר משתנים לפונקציה והם יודפסו לפי סדר שליחתם (כלומר הסדר בו הם מופיעים עם סיום המחרוזת):

let x = 5;
let y = 10;
println!("the value of x is: {} and the value of y is: {}", x, y);

// output:
// the value of x is 5 and the value of y is 10

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

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

let x = 5;
let y = 10;
println!("{0} is less than {1} and {1} is greater than {0}", x, y);

// output:
// 5 is less than 10 and 10 is greater than 5


 

עכשיו תורכם:

נסו לשלוח ל-println!() משתנים עם מספר סידורי לא מוגדר. למשל, שלחו שני משתנים בלבד והשתמשו במספרים {1} ו-{2}. נסו לקמפל את התוכנית ולהריץ אותה. מה קרה?

הגדרת מפתחות עבור משתנים עריכה

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

println!("the value of the variable is: {key}", key=variable);

בדוגמה האחרונה שלחנו לפונקציה את המשתנה variable והגדרנו את key למפתח שלו.

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


 

עכשיו תורכם:

נסו לשלוח ל-println!() משתנים עם מפתח לא מוגדר. עשו זאת בשני שלבים:
 •  ראשית, שלחו לפונקציה משתנה עם מפתח לא מוגדר. קמפלו את התוכנית והריצו אותה. מה קיבלתם?
 •  אחר כך קראו לפונקציה פעמיים - בקריאה הראשונה שלחו לה משתנה עם מפתח מוגדר. בקריאה השנייה שלחו לה משתנה עם אותו המפתח של המשתנה בקריאה הקודמת. קמפלו את התוכנית והריצו אותה. מה קיבלתם הפעם?

תכונות נוספות של פונקציות ההדפסה (הרחבה) עריכה

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

println!("{0} in decimal base equals {0:x} in hexadecimal or {0:b} in binary", 100);

// output:
// 100 in decmial base equals 64 in hexadecimal or 1100100 in binary

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

println!("{:.2}", 3.141459265); // will print only 2 digits after the dot

// output:
// 3.14

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

println!("{:3}", 1); // will add two white spaces before the the digit
println!("{:3}", 100); // will have no effect - 100 is already 3 digits long

// output:
//   1
// 100

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

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

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

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

fn main() {
    let x: i32 = 5;
    {
        let y: i32 = 4;
        println!("The value of x is {} and value of y is {}", x, y);
    }
    println!("The value of x is {} and value of y is {}", x, y); // This won't work.
}

כפי שאנו רואים בדוגמה, הגדרנו בתוכנית שני בלוקים - בלוק אחד שמסמל את רצף הפקודות של פונקציית ה-main ובלוק שני ללא ייחוד כלשהו. בבלוק ההוראות של פונקציית ה-main הגדרנו את המשתנה x (ולכן אורך החיים שלו הוא בין הסוגריים המסולסלים של פונקציית ה-main) ובבלוק הנוסף הגדרנו את המשתנה y (ולכן אורך החיים שלו הוא בין הסוגריים המסולסלים של בלוק זה).

אם ננסה להדפיס את x ו-y כמו בקריאה הראשונית לפונקציית println!() בדוגמה, נצליח, מפני שאורך החיים של שני המשתנים תקף בבלוק השני. עם זאת, אם ננסה להדפיס את המשתנים מחוץ לבלוק המשותף, כמו בקריאה השנייה של הפונקציה, הקוד לא יתקמפל וזאת משום שאורך החיים של y הוא בתוך הבלוק המשותף בלבד ולכן כשנצא ממנו המעבד לא יכיר את המשתנה y.

הצללה (Shadowing) עריכה

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

let x = 5;
let x = 3.5; // `x` is now a float variable

תחילה הגדרנו משתנה בשם x מטיפוס i32. אחר כך, הצהרנו על משתנה חדש באותו השם, בעל טיפוס אחר (זכרו שמספרים עשרוניים מיוצגים על ידי טיפוסי float ובעת הגדרה לא מופרשת כמו בדוגמה, הם מיוצגים על ידי f64). עם ההצהרה על המשתנה החדש, ביצענו גם הצללה למשתנה הקודם - כלומר, כל התכונות של המשתנה הקודם אבדו.

נמחיש את פעולת ההצללה בעזרת דוגמה נוספת:

let mut x = 4;
x = 8;
let x = x; // `x` is now immutable and is bound to `8`.

כפי שתוכלו לראות, הצהרנו על משתנה בשם x שהוא mutable - כלומר, משתנה שניתן לשנות את ערכיו במהלך התוכנית. בהמשך, ביצענו פעולת השמה למשתנה והצבנו בו את המספר 8. אחרי כן, ביצענו הצללה (הגדרנו משתנה חדש בשמו של הקודם) ואתחלנו אותו עם המספר 8 שהיה שמור קודם לכן במשתנה x. שימו לב שבפעולת ההצללה איבד המשתנה הישן את כל התכונות שלו - גם את תכונות ה-mutability שלו. x הוא כעת immutable ולכן אם ננסה לבצע לו פעולת השמה נקבל שגיאת קומפילציה.

הצללה לעומת mutability עריכה

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

let mut x = 4;
x = 7.5; // error: mismatched types

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

קבועים עריכה

בשפת ראסט ניתן להצהיר גם על משתנים קבועים. משתנים קבועים הם משתנים בעלי ערך קבוע, כלומר, מרגע ההצהרה עליהם, לא ניתן לשנות את ערכם. מילית ההצהרה על משתנים קבועים היא "const" (קיצור של constant - קבוע). בשונה מדרך ההצהרה על משתנים רגילים באמצעות המילית "let", בעת הצהרה על משתנים קבועים יש לספק את סוג הטיפוס של הקבוע. לדוגמה:

const NUMBER_OF_ELEMENTS: i32 = 5;
 

קונבנציה (מוסכמה):

כדי להבדיל בין משתנים רגילים לקבועים, תמיד נצהיר על קבועים באותיות גדולות (A-Z).
לדוגמה: MAX, MIN, NUM1.

קבועים לעומת משתנים מסוג immutable עריכה

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

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

הסבה (Casting) עריכה

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

let mut x = 10.5;
x = 3; // error

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

פתרון 1: הוספת נקודה אפס (0.) עריכה

דרך אחת להודיע לקומפיילר להתייחס למספר 3 כאל מספר עשרוני היא באמצעות הוספת נקודה אפס (0.) אחרי ה-3. כלומר, במקום שנבצע פעולת השמה לערך 3, נבצע פעולת השמה לערך 3.0 ואז הקומפיילר יבין שמדובר במספר עשרוני.

let mut x = 10.5;
x = 3.0;
 

כדאי לדעת:

זוכרים שבסוף השיעור הקודם הגדרנו קונבנציה לפיה יש לאתחל משתנה מסוג מספר עשרוני במספר 0.0? עכשיו ודאי הבנתם את הסיבה:
אם נאתחל את המשתנה במספר 0 ונצהיר על הסוג שלו כעל f64/f32, הקומפיילר לא יצליח לבצע פעולת השמה למשתנה, משום שהוא מתייחס למספר 0 כאל מספר שלם. לכן, עלינו להבהיר לקומפיילר שאנו רוצים להגדיר משתנה מסוג מספר עשרוני ולא מספר שלם ונשתמש ב0.0.

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

פתרון 2: שימוש במילית as עריכה

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

let x: i32 = 5;
let y = x as i64;

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

שימו לב! ביצוע הסבה למשתנה באמצעות המילית as עלול להוביל לאיבוד מידע. הריצו את דוגמת הקוד הבאה:

let x = 5.8;
let y = x as i64;
println!("y = {}", y); // prints "y = 5"

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

תרגול עריכה

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


הפרק הקודם:
משתנים
פלט והצללה
תרגילים
הפרק הבא:
פעולות אריתמטיות