משתנים שאי אפשר לשנות (בערך)

3/8/2021, לפני חודשיים
תגיות: rust, ראסט

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

הצהרה על משתנה

ניצור פרויקט חדש (אם לא זוכרים כיצד, אפשר להסתכל על הפוסט הקודם ולהיזכר איך לעשות את זה בעזרת קרגו). נשנה את הקוד בקובץ src/main.rs לקוד הבא:

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

נריץ את התוכנה המדהימה שכתבנו באמצעות הפקודה

cargo run
…
The value of x is: 5

יש! הכל עבד כצפוי.

נעבור על מה עשינו כאן: בשורה 1 הצהרנו כרגיל על הפונקציה main, שלא מקבלת שום ארגומנטים. בשורה 2 הצהרנו על משתנה בשם x באמצעות מילת המפתח let, ונתנו לו את הערך 5. בשורה 3 השתמשנו במקרו println והדפסנו למסך את המשפט The value of x is: 5, כאשר במקום הסוגריים המסולסלים יבוא הייצוג של x כמחרוזת (String), כיוון שהוא המשתנה שנמצא אחרי הפסיק.

אבל רגע! אם נסתכל בוויקיפדיה, נראה שראסט היא שפה סטטית, ולא דינמית, כלומר הטיפוס (או המילים אחרות, הסוג) של המשתנה x חייב להיות ידוע כבר בזמן הקימפול. אז איך זה כן עובד? הקומפיילר (rustc) מסיק את הטיפוסים של המשתנים כל פעם שהוא מסוגל (שזה רוב המקרים). בדוגמה שלנו, הקומפיילר יסיק שהטיפוס של x הוא i32 - כלומר Integer בגודל 32 בתים signed (כשהכוונה ב”סימן” פה הכוונה ליכולת להיות שלילי). אם נרצה לקבוע את הטיפוס של המשתנה במפורש, נשנה את הקוד שלנו:

let x: u64 = 5;

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

let x: u64 = -5;

אם ננסה את להריץ את התוכנה עכשיו, נקבל את השגיאה הבאה:

cargo run
   Compiling …
error[E0600]: cannot apply unary operator `-` to type `u64`
 --> src/main.rs:2:18
  |
2 |     let x: u64 = -5;
  |                  ^^ cannot apply unary operator `-`
  |
  = note: unsigned values cannot be negated

error: aborting due to previous error

For more information about this error, try `rustc --explain E0600`.

השגיאה עצמה כתובה באנגלית ממש פשוטה, וזה, לעניות דעתי, אחת החוזקות של ראסט – גם כשכבר עושים טעות, די קל להבין מה הטעות ואיפה בדיוק היא נמצאת, כי הודעות השגיאה של הקומפיילר ברורות למדי - כתוב בדיוק באיזו שורה השגיאה (2), ומה בדיוק השגיאה היתה: note: unsigned values cannot be negated - בדיוק כמו שציפינו.

ניתן למצוא את רשימת הטיפוסים המלאה של ראסט בתיעוד.

השמה למשתנה

ברוב שפות התכנות, אפשר להכריז על משתנים, ולא לתת להם ערך כלל. למשל, בשפת C:

#include <stdio.h>

int main()
{
   int x;
   printf("%d", x);

    return 0;
}

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

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

נקבל את השגיאה:

println!("The value of x is: {}", x);
                                  ^ use of possibly-uninitialized `x`

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

משתנה שאי אפשר לשנות – Imutability

ברוב שפות התכנות, לאחר שהצהרנו על משתנה, ניתן לשנות את הערך ששמור בו, למשל:

 #include <stdio.h>

int main()
{
    int x = 5;
    printf("%d\n", x);
    x = 6;
    printf("%d", x);
    return 0;
}

ננסה לכתוב משהו דומה בראסט:

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

התכונה תסרב להתקמפל ונקבל שגיאה:

cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

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

על מנת שנוכל להציב למשתנה, עלינו להצהיר עליו בצורה קצת שונה:

let mut x = 5;

באמצעות מילת המפתח mut הפכנו את הטיפוס של המשתנה x מסוג i32 לסוג mut i32, ולכן אם נריץ את התוכנה שלנו עכשיו, נקבל את התוצאה הצפויה:

cargo run
…
The value of x is: 5
The value of x is: 6

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

let mut x = 5;
x = "string";

עדיין תכשל עם שגיאה:

error[E0308]: mismatched types
 --> src/main.rs:3:9
  |
3 |     x = "string";
  |         ^^^^^^^^ expected integer, found `&str`

error: aborting due to previous error

הערות, מענות וכו'
נכתב על ידי אסף ספיר מתכנת, לשעבר פרמדיק ואח.
© Assaf Sapir, 2021, Built with Gatsby. Hosted with GitHub Pages.
Source code on my GitHub.