2.5 - הפשטות גנריות התלויות בזמן חיים - הרצאה
תוצאה שחיה רק עד הקריאה הבאה¶
נניח שמקור נתונים מחזיק בעצמו buffer ומחזיר מבט לתוכו. אחרי שהקורא סיים עם המבט, המקור רשאי להתקדם ולהשתמש שוב באחסון. כאן סוג התוצאה אינו רק בחירה קבועה של המקור: הוא תלוי בזמן החיים של ההשאלה הנוכחית למקור.
ה־trait Iterator הרגיל מגדיר Item אחד לכל מימוש. הוא מתאים גם לאיטרטורים שמחזירים הפניות למאגר חיצוני שנשאר חי, אבל אינו מבטא ישירות תוצאה שזמן חייה משתנה לפי כל השאלת self. לשם כך נשתמש בטיפוס קשור גנרי, או GAT.
trait LendingSource {
type Item<'a>
where
Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
struct Chunks {
bytes: Vec<u8>,
position: usize,
}
impl LendingSource for Chunks {
type Item<'a> = &'a [u8];
fn next<'a>(&'a mut self) -> Option<&'a [u8]> {
if self.position == self.bytes.len() {
return None;
}
let start = self.position;
let remaining = self.bytes.len() - start;
let end = start + remaining.min(2);
self.position = end;
Some(&self.bytes[start..end])
}
}
fn main() {
let mut source = Chunks { bytes: vec![1, 2, 3, 4, 5], position: 0 };
while let Some(chunk) = source.next() {
println!("{chunk:?}");
}
}
הסוג Item<'a> הוא משפחה של טיפוסים, ולא טיפוס אחד שמנותק מן ההשאלה. הקריאה מקבלת &'a mut self ומחזירה פרוסה עם אותו יחס זמן חיים. החסם Self: 'a מציין שתכולת Self צריכה להיות תקפה עבור זמן החיים הרלוונטי; הוא אינו הופך את המקור לבעל חיים אינסופיים.
למה אי אפשר לאסוף את כל המבטים¶
נסו להחליף את main בגרסה הבאה, שנועדה להידחות כאשר שתי התוצאות עדיין נחוצות:
fn main() {
let mut source = Chunks { bytes: vec![1, 2, 3, 4], position: 0 };
let first = source.next().unwrap();
let second = source.next().unwrap();
println!("{first:?} {second:?}");
}
הקריאה הראשונה מחזירה תוצאה שמחזיקה את ההשאלה של המקור. כדי לקרוא שוב נדרשת השאלה בלעדית חדשה, אבל הראשונה עדיין דרושה להדפסת first. המהדר מונע זאת לפי החוזה, גם אם המימוש המסוים שלנו אינו מחליף את ה־Vec בפועל.
ההגבלה מועילה כאשר מחליפים את Chunks במקור אמיתי שממלא מחדש buffer. כך הקורא אינו יכול לשמור בטעות הפניה לנתונים שיידרסו בקריאה הבאה. אם הוא צריך לשמור תוצאה לאורך זמן, הוא יכול ליצור עותק בבעלות בנקודה שבה הבעלות נעשית דרישה אמיתית.
לא כל אלגוריתם המבוסס על Iterator משתלב אוטומטית עם LendingSource. למשל, איסוף כל האיברים לאוסף רגיל עשוי לסתור את חוזה ההשאלה. כשמעצבים API כזה צריך להחליט אילו פעולות אפשריות ולספק עיבוד שמתאים לו, במקום להבטיח שכל combinator רגיל יעבוד ללא שינוי.
לפעמים הקורא צריך לקבל כל זמן חיים¶
נבחן בעיה אחרת: פונקציה יוצרת packet זמני ומאפשרת ל־callback לקרוא ממנו. הקורא אינו יכול לבחור מראש זמן חיים שיימשך מעבר למאגר המקומי. אנחנו רוצים פונקציה שמסוגלת לקבל הפניה בכל זמן חיים שהמימוש יבחר לקריאה.
fn with_packet<F>(callback: F)
where
F: for<'a> FnOnce(&'a [u8]),
{
let packet = [10u8, 20, 30];
callback(&packet);
}
fn main() {
with_packet(|packet| {
println!("length={} first={}", packet.len(), packet[0]);
});
}
הסימון for<'a> הוא חסם מדרגה גבוהה, או HRTB. הוא דורש שה־callback יתאים לכל זמן חיים רלוונטי של ההפניה, ולא רק לזמן חיים אחד שהקורא בחר עבור כל הקריאה החיצונית. היכולת לקרוא מן המאגר אינה כוללת הרשאה להוציא הפניה אליו אל מקום שחי יותר ממנו.
הדוגמה הבאה אמורה להיכשל כאשר משתמשים באותה with_packet:
fn main() {
let mut saved = None;
with_packet(|packet| {
saved = Some(packet);
});
println!("{saved:?}");
}
ה־callback מנסה לשמור את ההפניה אחרי שסביבת הקריאה הזמנית נגמרת. אם במקום זאת נשמור packet.to_vec(), נקבל נתונים בבעלות עצמאית, והיחס הלוגי ישתנה באופן אמיתי. החסם אינו אוסר לשמור מידע; הוא אוסר להעמיד פנים שההשאלה הזמנית נשארה תקפה.
קוראים חתימה מורכבת דרך שאלות פשוטות¶
כאשר רואים GAT, שאלו איזה טיפוס משתנה עם פרמטר זמן החיים ומי מספק את האחסון שלו. כאשר רואים HRTB, שאלו מי בוחר את זמן החיים והאם הפעולה צריכה להתאים לכל בחירה. ההבדל בין משפחת תוצאות לבין פעולה כללית על השאלות הוא הבדל תכנוני, לא רק תחבירי.
בפרקי אסינכרוניות נפגוש שוב טיפוסים שהתוצאה שלהם תלויה בזמן החיים של משאב. לפני שמגיעים אליהם כדאי להתרגל לבדוק שתי תכונות של כל ממשק: האם ניתן לשמור את התוצאה, והאם ניתן להתחיל פעולה נוספת בזמן שהתוצאה הקודמת עוד בשימוש. הדוגמאות הקצרות כאן נותנות תשובות באמצעות החתימות עצמן.