1.4 - טקסט ואוספים ובעלות על האחסון שלהם - הרצאה
כמה תווים יש במחרוזת¶
נתחיל בתוכנית שנראית פשוטה:
fn main() {
let text = String::from("שלום");
println!("bytes={}", text.len());
println!("chars={}", text.chars().count());
}
המספר הראשון הוא שמונה והמספר השני הוא ארבעה. אין כאן באג בספירה. המתודה len של מחרוזת מחזירה את מספר הבייטים בייצוג UTF-8, ואילו האיטרטור chars עובר על ערכי Unicode scalar. כל אחת מארבע האותיות שבדוגמה מקודדת בשני בייטים.
שימו לב שגם מספר ערכי char אינו תמיד מספר הסימנים שהמשתמש תופס כתווים. אות וסימן ניקוד יכולים להופיע כשני ערכים נפרדים, ורצפים מסוימים מוצגים כסמל אחד. כאשר נבנה פרוטוקול נחשב אורכים בבייטים לפי הגדרתו. כאשר נבנה ממשק טקסט למשתמש נצטרך להחליט איזו יחידת טקסט הוא דורש.
מחרוזת אינה מערך של תווים ברוחב קבוע¶
נסו להחליף את גוף main בגישה text[0]. הקוד אינו מתקמפל משום שגישה מספרית כזאת אינה מגדירה אם אנחנו מבקשים בייט, ערך Unicode או סימן תצוגה. במקום להסתיר סריקה או להחזיר חלק לא תקין מקידוד, הממשק מבקש שנבחר במפורש.
fn main() {
let text = String::from("שלום");
println!("first byte={}", text.as_bytes()[0]);
println!("first char={:?}", text.chars().next());
println!("first letter={:?}", text.get(0..2));
println!("broken boundary={:?}", text.get(0..1));
}
הטווח הראשון מסתיים בגבול חוקי של קידוד ומחזיר את האות הראשונה. הטווח השני מסתיים באמצע הקידוד שלה ולכן מחזיר None. אם היינו משתמשים בחיתוך הישיר &text[0..1], היינו מקבלים פאניקה. הבייטים עצמם קיימים בזיכרון, אבל הטווח אינו מחרוזת UTF-8 חוקית.
ההבחנה הזאת חשובה בפענוח נתונים. קובץ או packet הם קודם כול רצף בייטים. רק לאחר בדיקת הקידוד אפשר להתייחס לחלק מהם כאל טקסט. כאשר המידע עשוי להיות בינארי, אל תכריחו אותו להיכנס ל־String רק משום שקל להדפיס אותה.
אורך קיבולת והקצאה מחדש¶
הטיפוס String מחזיק טקסט בבעלות, והטיפוס Vec מחזיק אוסף רציף בבעלות. בשניהם נבדיל בין מספר הבייטים או האיברים הפעילים לבין הקיבולת שכבר הוקצתה. הקיבולת מאפשרת להוסיף נתונים בלי לבקש הקצאה חדשה בכל הוספה, אבל אין להניח מהו קצב הגדילה המדויק של המימוש.
הריצו:
fn main() {
let mut values = Vec::<u64>::with_capacity(2);
for value in 0..8 {
values.push(value);
println!("len={} cap={} ptr={:p}",
values.len(), values.capacity(), values.as_ptr());
}
}
התבוננו באורך, בקיבולת ובכתובת. הקיבולת עשויה לגדול כשהאורך מגיע לגבול, והכתובת עשויה להשתנות. שינוי כתובת הוא ראיה שהאחסון עבר, אבל אי־שינוי כתובת בהרצה אחת אינו הבטחה שההקצאה הבאה תישאר במקומה. המקצה עשוי להרחיב אזור קיים או להחזיר אזור אחר.
זו אחת הסיבות לכך שגישה לאיבר והגדלת אותו Vec אינן תמיד יכולות להתקיים יחד. ההפניה לאיבר מתארת מקום מסוים, ופעולת ההגדלה עשויה להפוך את המקום הזה ללא רלוונטי. בשיעור ההשאלה נבדוק כיצד המהדר מזהה את הקשר בלי להריץ את המקצה.
בונים פלט בלי לשכפל הכול בכל צעד¶
נרצה לחבר רשימת שמות לפלט אחד. אפשר להחזיק מחרוזת בבעלות ולהוסיף אליה טקסט מושאל:
fn join_names(names: &[&str]) -> String {
let mut output = String::new();
for (index, name) in names.iter().enumerate() {
if index != 0 {
output.push(',');
}
output.push_str(name);
}
output
}
fn main() {
println!("{}", join_names(&["alpha", "beta", "gamma"]));
}
הפונקציה מחזיקה את הפלט ומקבלת רק מבטים לשמות. הפעולה push_str מעתיקה את הבייטים של כל שם אל הפלט, אך אינה דורשת שכפול של כל מחרוזת הקלט לאובייקט עצמאי קודם לכן. כאשר יודעים מראש את הגודל הכולל אפשר לשקול הזמנת קיבולת, עם בדיקת גלישה בחישוב. אין צורך לבצע חישוב נוסף אם העלות שלו אינה מוצדקת לעומס שלנו.
בוחרים אוסף לפי הפעולה¶
כאשר נרצה לשמור מונה לכל שם, HashMap מאפשרת לחפש לפי מפתח. כאשר נרצה לעבור על המפתחות בסדר, BTreeMap עשויה להתאים יותר. הבחירה אינה נובעת רק משם המבנה או מסיבוכיות שנלמדה בעל פה, אלא גם מסדר הגישה, מגודל הנתונים וממידת הצורך בפלט יציב.
דוגמה מלאה לספירה עם מפתחות מושאלים:
use std::collections::BTreeMap;
fn main() {
let input = "red blue red green";
let mut counts = BTreeMap::new();
for word in input.split_whitespace() {
*counts.entry(word).or_insert(0usize) += 1;
}
for (word, count) in counts {
println!("{word}={count}");
}
}
המפתחות בדוגמה הם מבטים לתוך input, ולכן המקור חייב להישאר חי כל עוד המפה משתמשת בהם. אם נקרא שורות ל־buffer אחד ונמחזר אותו בכל סיבוב, לא נוכל לשמור מבטים ישנים כאילו הנתונים נשארים ללא שינוי. במקרה כזה צריך להחליט אם המפה תהיה בעלת המילים, אם נשמור את כל המקור, או אם נשנה את מבנה העיבוד.
התרגול יעזור לכם לחבר בין שלוש שאלות שנראות נפרדות: באיזו יחידה אנחנו סופרים, מי מחזיק את הנתונים, ואיזו פעולה יכולה לשנות את מקום האחסון. אלה שאלות יומיומיות בתוכנת מערכת, גם כשאין בקוד שום מצביע גולמי.