2.1 - תכונות גנריות וטיפוסים קשורים - הרצאה
אותו אלגוריתם עם יעדי פלט שונים¶
נניח שכתבנו פונקציה שמייצאת רשומות לקובץ. עכשיו אנחנו רוצים לבדוק אותה בלי ליצור קובץ, ובהמשך לשלוח את אותו פלט דרך חיבור. אם הפונקציה בונה File בעצמה, האלגוריתם קשור להחלטה על המשאב. אפשר להפריד ביניהם באמצעות trait שמגדיר איזו יכולת האלגוריתם צריך.
הספרייה הסטנדרטית כבר מגדירה את Write. נכתוב פונקציה שמקבלת יעד שמממש אותו:
use std::io::{self, Write};
fn write_record<W: Write>(output: &mut W, name: &str, value: u64) -> io::Result<()> {
writeln!(output, "{name}={value}")
}
fn main() -> io::Result<()> {
let mut memory = Vec::new();
write_record(&mut memory, "jobs", 12)?;
assert_eq!(memory, b"jobs=12\n");
let stdout = io::stdout();
write_record(&mut stdout.lock(), "jobs", 12)
}
האלגוריתם אינו יודע אם היעד הוא מאגר בזיכרון או משאב מערכת. הוא כן יודע שהטיפוס מקיים את חוזה Write, ולכן מותר לו להשתמש בפעולות שהממשק מגדיר. הקריאה עם Vec עדיין נבדקת בזמן קומפילציה מול טיפוס קונקרטי; אין כאן בהכרח חיפוש מתודה דינמי בזמן ריצה.
הפונקציה מקבלת השאלה בלעדית ליעד. היא אינה לוקחת בעלות על הקובץ או על המאגר, ולכן הקורא יכול להמשיך להשתמש בהם לאחר סיום הקריאה. כך החתימה מבטאת גם את היכולת הדרושה וגם את יחסי הבעלות.
תכונה היא חוזה ולא שם של מחלקת אב¶
נגדיר trait משלנו שמייצג מקור של ערך אחד בכל קריאה:
trait Source {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
current: u32,
end: u32,
}
impl Source for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current >= self.end {
return None;
}
let value = self.current;
self.current += 1;
Some(value)
}
}
fn sum<S>(source: &mut S) -> u64
where
S: Source<Item = u32>,
{
let mut total = 0;
while let Some(value) = source.next() {
total += u64::from(value);
}
total
}
fn main() {
let mut source = Counter { current: 0, end: 4 };
assert_eq!(sum(&mut source), 6);
}
הטיפוס הקשור Item נבחר על ידי המימוש של Source עבור Counter. האלגוריתם sum דורש שהבחירה הזאת תהיה u32. אם מקור אחר מחזיר מחרוזות, הוא עדיין יכול לממש Source, אבל אינו מתאים לפונקציה שמחברת מספרים.
הבלוק where אינו מוסיף בדיקה בזמן ריצה. הוא מבטא תנאים שהמהדר צריך להוכיח לכל שימוש בפונקציה. כאשר האבחון אומר שהטיפוס אינו מקיים bound, שאלו איזה חלק בחוזה האלגוריתם לא התקיים. הוספת תכונה שרירותית רק כדי לסגור אבחון עלולה לצמצם את הממשק בלי סיבה.
מי בוחר את הפרמטר¶
אפשר להגדיר גם trait עם פרמטר טיפוס, למשל Convert<T>. במקרה כזה אותו טיפוס עשוי לממש את התכונה עבור כמה ערכי T שונים, כל עוד כללי המימוש מאפשרים זאת. לעומת זאת, במימוש מסוים של Source עבור Counter יש בחירה אחת של Item.
השאלה ההנדסית היא האם סוג התוצאה הוא החלטה של המקור או אפשרות שהקורא בוחר כחלק מן הפעולה. עבור מקור רשומות אחד, טיפוס רשומה קשור לרוב למקור. עבור המרה ממספר לכמה צורות פלט, פרמטר טיפוס יכול לבטא כמה פעולות שונות. הבחירה משפיעה על הסקת טיפוסים ועל האפשרות להוסיף מימושים בעתיד.
הנה דוגמה נפרדת שמראה שתי המרות חוקיות:
trait Convert<T> {
fn convert(&self) -> T;
}
struct Count(u32);
impl Convert<u64> for Count {
fn convert(&self) -> u64 { u64::from(self.0) }
}
impl Convert<String> for Count {
fn convert(&self) -> String { self.0.to_string() }
}
fn main() {
let count = Count(9);
let number: u64 = count.convert();
let text: String = count.convert();
println!("{number} {text}");
}
דרישות מינימליות שומרות אפשרויות פתוחות¶
נניח שהאלגוריתם רק מדפיס נתון, אבל דורש בנוסף Clone משום שהמימוש הראשון שלנו היה ניתן לשכפול. הדרישה הנוספת תדחה טיפוסים שאפשר להדפיס ושאסור לשכפל, אף שאין לאלגוריתם צורך אמיתי בשכפול. כל bound ציבורי הוא חלק מן ההתחייבות של API.
מצד שני, אי אפשר להשתמש בגוף הפונקציה בפעולה שלא הובטחה בחתימה רק משום שהטיפוסים הנוכחיים במקרה תומכים בה. המהדר בודק את הגוף הגנרי לפי החוזים שהוגדרו. לכן תכנון generic מתחיל בשאלה אילו פעולות דרושות לאלגוריתם, ורק אחר כך בחיבור הטיפוסים שיבצעו אותן.
נשתמש גם במתודות ברירת מחדל כאשר ניתן לנסח אלגוריתם על בסיס פעולות ליבה של התכונה. ההגדרה אינה מבטיחה שמימוש חלופי יקיים משמעות שלא נכתבה בחוזה. אם source מבטיח סדר או יציבות אחרי סוף הקלט, צריך לתעד זאת ולבדוק זאת בנפרד מן התחביר.
בהמשך נבחן את הקוד שנוצר עבור כמה מימושים ונשווה אותו לשיגור דינמי. לפני כן חשוב שתוכלו לקרוא S: Source<Item = u32> כמשפט על יכולת ועל בחירת טיפוס, ולא כרצף סימנים שהועתק מדוגמה. כללי ההגדרה מפורטים במפרט התכונות של ראסט.