לדלג לתוכן

2.6 - שונות וסמני טיפוס בתכנון מצבים - הרצאה

למה מותר לקצר הפניה אבל לא כל מעטפת שלה

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

fn shorter<'a>(value: &'static str) -> &'a str {
    value
}

fn main() {
    let text = shorter("fixed");
    println!("{text}");
}

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

אפשרות כתיבה משנה את התשובה

נניח שהיה מותר לקחת תא שמחזיק &'static str ולהתייחס אליו כתא שמחזיק הפניה קצרה. הקורא היה יכול לכתוב לתוכו טקסט מקומי, ואז הבעלים המקורי היה קורא אותו כאילו הוא static. התוצאה הייתה הפניה שנראית תקפה זמן רב יותר מן הנתון שלה.

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

use std::cell::Cell;

fn shorten_cell<'a, 'b>(cell: &'b Cell<&'static str>) -> &'b Cell<&'a str> {
    cell
}

fn main() {}

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

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

כיוון של קלט בפונקציה

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

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

מצב יכול להיות חלק מן הטיפוס

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

use std::marker::PhantomData;

struct Draft;
struct Ready;

struct Message<State> {
    body: String,
    state: PhantomData<State>,
}

impl Message<Draft> {
    fn new(body: String) -> Self {
        Self { body, state: PhantomData }
    }
    fn prepare(self) -> Result<Message<Ready>, &'static str> {
        if self.body.is_empty() { return Err("empty message"); }
        Ok(Message { body: self.body, state: PhantomData })
    }
}

impl Message<Ready> {
    fn send(self) {
        println!("sent {}", self.body);
    }
}

fn main() {
    let draft = Message::new("hello".into());
    match draft.prepare() {
        Ok(ready) => ready.send(),
        Err(error) => eprintln!("{error}"),
    }
}

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

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

מה הטיפוס לא מוכיח

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

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