לדלג לתוכן

1.8 - זמני חיים בחתימות ובמבנים - הרצאה

הפניה יוצאת מפונקציה אבל הנתון נשאר אצל הקורא

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

fn split_setting(line: &str) -> Option<(&str, &str)> {
    line.split_once('=')
}

fn main() {
    let input = String::from("name=worker");
    if let Some((key, value)) = split_setting(&input) {
        println!("key={key} value={value}");
    }
}

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

זמן חיים הוא יחס בין שימושים

אפשר לכתוב את החתימה בצורה מפורשת:

fn split_setting<'a>(line: &'a str) -> Option<(&'a str, &'a str)> {
    line.split_once('=')
}

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

נסו את התוכנית הבאה, שנועדה להיכשל:

fn split_setting(line: &str) -> Option<(&str, &str)> {
    line.split_once('=')
}

fn main() {
    let value;
    {
        let input = String::from("name=worker");
        value = split_setting(&input).unwrap().1;
    }
    println!("{value}");
}

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

שני מקורות דורשים החלטה

ננסה לכתוב פונקציה שמחזירה את הארוכה מבין שתי מחרוזות:

fn longer<'a>(left: &'a str, right: &'a str) -> &'a str {
    if left.len() >= right.len() {
        left
    } else {
        right
    }
}

fn main() {
    let left = String::from("alpha");
    let right = String::from("beta");
    println!("{}", longer(&left, &right));
}

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

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

fn choose_left<'a>(left: &'a str, _right: &str) -> &'a str {
    left
}

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

מבנה שמחזיק מבט לקלט

נארוז את תוצאת הפענוח במבנה:

#[derive(Debug)]
struct Setting<'a> {
    key: &'a str,
    value: &'a str,
}

fn parse(line: &str) -> Option<Setting<'_>> {
    let (key, value) = line.split_once('=')?;
    Some(Setting { key, value })
}

fn main() {
    let input = String::from("mode=fast");
    let setting = parse(&input).expect("expected key=value");
    println!("{} -> {}", setting.key, setting.value);
}

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

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

הפניה מתוך בעל הערך

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

struct Document {
    text: String,
}

impl Document {
    fn text(&self) -> &str {
        &self.text
    }
}

fn main() {
    let document = Document { text: "hello".into() };
    println!("{}", document.text());
}

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