1.5 - העברת בעלות העתקה ושכפול - הרצאה
אותה השמה יכולה לשנות את הבעלות¶
נתחיל בשתי השמות שנראות כמעט זהות:
fn main() {
let number = 17u32;
let other_number = number;
println!("{number} {other_number}");
let text = String::from("hello");
let other_text = text;
println!("{other_text}");
}
המספר המקורי נשאר זמין אחרי ההשמה. המחרוזת המקורית כבר אינה זמינה לשימוש אחרי שהעברנו אותה ל־other_text. נסו להוסיף הדפסה של text בסוף וקראו את האבחון. ההבדל אינו תלוי בשם המשתנה, וגם אינו נובע מכך שמחרוזות גדולות יותר ממספרים. הוא נובע מחוזי הטיפוסים.
הטיפוס u32 מקיים את התכונה Copy. העתקה של ערך כזה מותרת בלי לבטל את האפשרות להשתמש במקור. הטיפוס String אינו מקיים אותה, משום שהוא מחזיק בעלות על הקצאה. העתקה לא מבוקרת של תיאור ההקצאה הייתה משאירה שני בעלים שחושבים שעליהם לשחרר את אותו אזור.
העברה אינה שם נרדף להעתקה פיזית¶
הפעולה move היא שינוי לוגי בשאלה מאיזה מקום מותר להשתמש בערך. המהדר עשוי להעתיק בייטים כדי לממש אותה, והוא עשוי גם לגלות שאין צורך בשום העתקה. למשל, האופטימיזציה יכולה לבנות ערך ישירות במקום שבו יישמר בהמשך. לכן אין להסיק מן המשפט שהערך הועבר כמה הוראות זיכרון ירוצו בפועל.
חשבו על String כתיאור של אחסון שבבעלותה. לצורך המודל שלנו יש בו מצביע, אורך וקיבולת, אך אין להשתמש בתיאור הזה כדי לנחש סדר שדות או להעתיק את הייצוג דרך FFI. כאשר הבעלות עוברת, האחריות להקצאה עוברת איתה. העתקה של מצביע גולמי לבדה אינה מעבירה אחריות בשום מערכת טיפוסים.
נבנה פונקציה שמקבלת בעלות ומחזירה אותה:
fn add_suffix(mut text: String) -> String {
text.push_str(".log");
text
}
fn main() {
let original = String::from("events");
let path = add_suffix(original);
println!("{path}");
}
הבעלות עוברת מן המשתנה original לפרמטר, ואז מן הפרמטר לערך החזרה. הפונקציה יכולה להגדיל את המחרוזת, והמקצה עשוי להעביר את האחסון. כל אלה פרטים נפרדים מן השאלה מי מחזיק בערך בכל שלב. המשתנה original אינו חוזר להיות תקף רק משום שהפונקציה החזירה מחרוזת שנבנתה ממנו.
שכפול הוא פעולה בעלת משמעות¶
המתודה clone יוצרת ערך נוסף לפי חוזה Clone של הטיפוס. עבור String מתקבל טקסט עצמאי, ולכן אפשר לשנות עותק אחד בלי לשנות את האחר:
fn main() {
let first = String::from("worker");
let mut second = first.clone();
second.push_str("-2");
println!("first={first}");
println!("second={second}");
}
אין להסיק שכל clone מעתיק את כל הנתונים הפנימיים. בהמשך נפגוש טיפוסי בעלות משותפת שבהם clone מגדיל מונה ומשתף את אותו אובייקט. התכונה אומרת שאפשר ליצור ערך נוסף לפי חוזה הטיפוס, ולא שהפעולה בהכרח יקרה או בהכרח יוצרת אחסון נפרד לכל מידע שאליו אפשר להגיע.
הדרך לבחור אינה לספור כמה פעמים המהדר הציע clone. שאלו האם באמת צריך שתי בעלויות עצמאיות, האם צריך שיתוף, או האם הפונקציה רק קוראת את הנתון. שימוש בשכפול כדי להימנע משאלה על חיי הנתונים יכול להסתיר תכנון לא מתאים, אפילו כאשר התוכנית נעשית תקינה מבחינת זיכרון.
יוצרים טיפוס שניתן להעתיק¶
הדוגמה הבאה מגדירה מזהה קטן:
#[derive(Clone, Copy, Debug)]
struct JobId {
generation: u32,
slot: u32,
}
fn main() {
let id = JobId { generation: 3, slot: 9 };
let saved = id;
println!("original={id:?} saved={saved:?}");
println!("slot={} generation={}", id.slot, id.generation);
}
שני השדות ניתנים להעתקה ואין במבנה destructor, ולכן אפשר לממש את Copy. אם נוסיף שדה String, הבקשה להפיק Copy תידחה. כדאי לזכור שגם כאשר השפה מאפשרת העתקה, ההחלטה לחשוף אותה היא החלטת API. משתמשי הספרייה עשויים להתחיל להסתמך עליה, ולכן הסרתה בגרסה מאוחרת יכולה לשבור קוד.
העברה חלקית של מבנה¶
אפשר להעביר שדה אחד מתוך מבנה ולהמשיך להשתמש בשדה אחר כאשר החוזים מאפשרים זאת:
struct Request {
name: String,
priority: u8,
}
fn main() {
let request = Request {
name: String::from("backup"),
priority: 2,
};
let name = request.name;
println!("name={name} priority={}", request.priority);
}
השם עבר אל המשתנה name, אבל השדה priority עדיין זמין. לעומת זאת, לא ניתן להעביר כעת את request כערך שלם כאילו כל השדות עדיין קיימים בו. בהמשך נראה שגם קיומו של Drop על הטיפוס משנה מה אפשר לעשות, משום שה־destructor עשוי להזדקק לכל המבנה בסיום חייו.
שימו לב שאין כאן מנגנון שבודק בעלות באמצעות מונה בכל השמה. המהדר מנתח את הקוד ואת הטיפוסים ומונע מסלולים שאינם חוקיים. המחיר בזמן ריצה תלוי בפעולות שבחרנו לבצע, כגון הקצאה או שכפול, ולא בעצם קיומו של כלל ההעברה.
בתרגול הבא נבנה מעבר מצב שבו הבעלות נראית בחתימה. המטרה היא שהקוד יספר מי רשאי להשתמש בבקשה אחרי ההעברה, בלי להוסיף דגל ידני שמנסה לעקוב אחרי אותו מידע בזמן ריצה.