2.2 - פולימורפיזם סטטי ודינמי והקוד שנוצר - הרצאה
בוחרים מימוש בזמן בנייה או בזמן ריצה¶
נניח שיש לנו שתי דרכים לנסח הודעת סטטוס. פונקציה גנרית יכולה לעבוד מול שתיהן, אבל לפעמים נרצה לבחור ביניהן לפי תצורה שמגיעה בזמן ריצה. הבחירה הזאת משפיעה על האופן שבו הקוד מתאר את הטיפוס ועל הדרך שבה מגיעים למתודה הנכונה.
נכתוב ממשק קטן ושני מימושים:
trait Describe {
fn describe(&self) -> String;
}
struct Short;
struct Detailed;
impl Describe for Short {
fn describe(&self) -> String { "ready".into() }
}
impl Describe for Detailed {
fn describe(&self) -> String { "worker is ready for requests".into() }
}
fn print_static<T: Describe>(value: &T) {
println!("{}", value.describe());
}
fn print_dynamic(value: &dyn Describe) {
println!("{}", value.describe());
}
fn main() {
print_static(&Short);
print_static(&Detailed);
let selected: &dyn Describe = if std::env::args().len() > 1 {
&Detailed
} else {
&Short
};
print_dynamic(selected);
}
בקריאות הגנריות המהדר מכיר טיפוס קונקרטי לכל שימוש. הוא יכול ליצור קוד מותאם לכל טיפוס ולהשתמש במידע לצורך inlining ואופטימיזציה. בתיאור &dyn Describe הנתון עדיין קיים בטיפוס הקונקרטי שלו, אבל הממשק שמחזיק ההפניה מתאר יכולת בלי לחשוף את הטיפוס המדויק.
הנתון והמידע שמאפשר לקרוא לו¶
הפניה לאובייקט trait נושאת גישה לנתון ומטא־נתונים שמאפשרים לממש את הפעולות של התכונה עבורו. במימושים הרגילים אנחנו מדברים על vtable. הטבלה מאפשרת להגיע למתודות המתאימות, ומידע נוסף משמש לפעולות כמו השמדה של אובייקט מאוגד.
חשוב להפריד בין המודל הזה לבין הבטחה על מיקום כל שדה בטבלה. אין להשתמש ב־transmute כדי לנחש offsets ולבנות API ציבורי סביבם. הייצוג וה־Rust ABI אינם תחליף לחוזה C מפורש כאשר רוצים חיבור בין בינאריים שנבנו בנפרד.
אפשר למדוד את גודל ההפניות בלי לפרק אותן:
use std::mem::size_of;
trait Describe { fn describe(&self) -> String; }
struct Short;
impl Describe for Short {
fn describe(&self) -> String { "ready".into() }
}
fn main() {
println!("concrete={}", size_of::<&Short>());
println!("dynamic={}", size_of::<&dyn Describe>());
println!("{}", Short.describe());
}
הפלט הוא תצפית על היעד שנבחר. הוא מסביר למה הפניה כזאת עשויה להיות רחבה יותר מהפניה לטיפוס בגודל קבוע, אבל אינו מתיר לנו להניח שכל מצביע בכל ארכיטקטורה בנוי באותה צורה.
בעלות דינמית¶
הטיפוס Box יכול להחזיק בעלות על אובייקט דרך ממשק trait. כך פונקציית יצירה מחזירה סוג אחיד גם כאשר המימוש נבחר בזמן ריצה:
trait Describe { fn describe(&self) -> &'static str; }
struct Short;
struct Detailed;
impl Describe for Short {
fn describe(&self) -> &'static str { "ready" }
}
impl Describe for Detailed {
fn describe(&self) -> &'static str { "ready for work" }
}
fn create(detailed: bool) -> Box<dyn Describe> {
if detailed { Box::new(Detailed) } else { Box::new(Short) }
}
fn main() {
let selected = create(true);
println!("{}", selected.describe());
}
הדוגמה משתמשת בטיפוסים ריקים, ולכן אין להסיק ממנה ש־Box תמיד דורש הקצאה בגודל חיובי. עבור ערך שמכיל נתונים ממשיים יש לשקול גם את עלות האחסון. ההבחנה החשובה כאן היא שהערך מוחזק בבעלות, ומנגנון ההשמדה יודע להתייחס לטיפוס הקונקרטי שנמצא מאחורי הממשק.
לא כל trait מתאים ל־dyn¶
נניח שנוסיף לתכונה מתודה גנרית שמקבלת כל טיפוס T. ממשק דינמי צריך לדעת אילו פעולות זמינות בטבלה, ואי אפשר פשוט ליצור מראש כניסה לכל T שהקוד עשוי לבחור בעתיד. לכן קיימים כללי dyn compatibility שמגבילים אילו מתודות ניתנות לשיגור כזה.
הקוד הבא נועד להידחות:
trait Store {
fn put<T>(&mut self, value: T);
}
fn use_store(_store: &mut dyn Store) {}
fn main() {}
אחת האפשרויות היא להחליף את המתודה הגנרית בחוזה קונקרטי, למשל קבלת פרוסת בייטים. אפשרות אחרת היא להשאיר מתודה גנרית שזמינה רק כאשר Self: Sized, ולתת לאובייקט הדינמי ממשק מצומצם יותר. הבחירה תלויה בשאלה איזו גמישות דרושה בזמן ריצה, ולא ברצון להכניס כל מתודה ל־vtable.
מודדים את ההשלכות הנכונות¶
שיגור סטטי יכול לאפשר מיטוב טוב יותר באתר הקריאה, אבל ריבוי טיפוסים עשוי להגדיל קוד וזמן בנייה. שיגור דינמי מאפשר להחזיק אוסף של מימושים שונים מאחורי ממשק אחיד, אבל מוסיף נקודת עקיפה ועשוי להגביל inlining. בפועל המהדר לפעמים יודע להסיר גם עקיפה דינמית כאשר המימוש גלוי לו.
לכן ניסוי ביצועים צריך לשמור עבודה אמיתית ולוודא שלא נמחקה. לפני מדידת זמן הסתכלו באסמבלי או בסמלים כדי להבין מה אתם משווים. בקורס נשתמש ב־dyn כאשר בחירת המימוש בזמן ריצה היא חלק מן הדרישה, ונעדיף חוזה גנרי כאשר הטיפוס ידוע והפשטה סטטית מספיקה.