למה Generics
מהר מאוד נתקלים בטיפוסים שצריכים לעבוד עם יותר מטיפוס איבר אחד. "זוג" של שני ערכים לא מתעניין אם הערכים הם מספרים שלמים, מחרוזות או shape כלשהו שהמשתמש הגדיר. Generics מאפשרים לכתוב את הטיפוס פעם אחת וליצור ממנו מופע עם כל טיפוס איבר שהקורא צריך.
החלופה, לכתוב IntPair, StringPair, BytePair וכן הלאה, נהיית משעממת מהר ולא מתחברת היטב. Generics ב-Zero הם הכלי הסטנדרטי לעבודה הזו.
פונקציות גנריות
מצהירים על פרמטרי טיפוס בסוגריים משולשים בין שם הפונקציה לרשימת הפרמטרים:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T ו-U הם טיפוסים ממלאי מקום: הקורא מחליט מה הם.
בקריאות בדרך כלל לא צריך לכתוב אותם; הקומפיילר מסיק אותם מטיפוסי הארגומנטים:
let pair = makePair(40, 2_u8)
כאן הקומפיילר מסיק ש-T הוא i32 (ברירת המחדל לליטרל שלם בלי סיומת) וש-U הוא u8 (מהסיומת _u8). לקישור שנוצר, pair, יש טיפוס Pair<i32, u8>.
אם ההסקה הייתה בוחרת טיפוסים שגויים, למשל כי הליטרלים דו-משמעיים, אפשר לקבוע את הפרמטרים במקום הקריאה:
let pair = makePair<u8, u8>(1, 2)
(ייתכן שתחביר הקריאה עם סוגריים משולשים לא יהיה בדיוק כמו שמוצג כאן בכל גרסאות Zero; בדקו בתיעוד העדכני את הכתיב המדויק. ההתנהגות שמעדיפה הסקה היא החלק היציב.)
Shapes גנריים
Shapes מקבלים פרמטרי טיפוס באותה דרך:
shape Pair<T, U> {
left: T,
right: U,
}
הטיפוס של כל שדה יכול להזכיר את הפרמטרים. המופעים קובעים אותם:
let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hi", right: "there" }
דוגמה מלאה שמשלבת shape גנרי ופונקציה גנרית. לחצו על Run כדי לראות את ההסקה בפעולה:
הצהרה אחת על shape גנרי, פונקציה גנרית אחת, מקום קריאה אחד עם טיפוסים חזקים. בלי קוד תבניתי של IntBytePair.
כינויי טיפוס
כשאותו טיפוס עם פרמטרים חוזר שוב ושוב, תנו לו שם עם type:
type BytePair = Pair<u8, u8>
עכשיו BytePair ניתן להחלפה עם Pair<u8, u8> בכל מקום שבו אפשר לכתוב טיפוס:
כינוי הוא רק פיצ'ר של שמות: הוא לא יוצר טיפוס נפרד. פונקציה שמקבלת BytePair תקבל בשמחה ערך מטיפוס Pair<u8, u8> (וגם להפך).
Generics בספרייה הסטנדרטית
אותו מנגנון מפעיל חלק גדול מהספרייה הסטנדרטית. כמה דוגמאות שתראו בקוד Zero אמיתי:
Maybe<T>: ערך אופציונלי, שמחזיקTאו כלום.Span<T>: פרוסה מושאלת של ערכיT.Span<u8>הוא התצוגה הקנונית של מאגר בתים.ref<T>ו-mutref<T>: טיפוסי הפניה מפורשים למקרים שבהם צריך לשתף נתונים בלי להעתיק אותם.
לא צריך ללמוד את כולם בבת אחת. כל הרעיון של generics הוא שאותו מבנה עובד עם כל טיפוס איבר שיש לכם ביד.
מתי Generics משתלמים (ומתי לא)
השתמשו ב-generics כשאתם מוצאים את עצמכם כותבים את אותה פונקציה או את אותו shape פעמיים עם טיפוסי איבר שונים. בחרו בטיפוס קונקרטי כש:
- הלוגיקה של הפונקציה הגיונית רק עבור טיפוס מסוים (למשל מנתח עבור
String). - אתם רוצים שהטיפוס יופיע בהודעות שגיאה כדי שדיבוג יהיה קל יותר.
- מאפייני הביצועים תלויים בגודל מסוים בזיכרון.
למחיר של generics יש משמעות: קבצים בינאריים גדולים יותר (כל מופע מייצר קוד חדש) וזמני קומפילציה מעט ארוכים יותר. ברוב קוד האפליקציות המחיר הזה זניח, אבל כדאי להכיר אותו כשבונים קוד הדוק בסגנון מערכות משובצות, שבו גודל הקובץ הבינארי חשוב.
הערה על מגבלות
מערכות גנריות מסוימות מאפשרות להגביל פרמטר טיפוס ("T חייב לתמוך ב-==", "T חייב לממש Iterator"). סיפור המגבלות של Zero שלפני 1.0 עדיין מתפתח: הדוגמאות במאגר הרשמי משתמשות ב-generics בצורתם הפשוטה, בלי גבולות מורכבים. ככל שהשפה מתייצבת, צפו שתחביר המגבלות יגיע בצורה קטנה וסדירה שעקבית עם שאר השפה. בינתיים, כתבו generics שעובדים עם כל T שאתם באמת מעבירים להם, ותנו לקומפיילר לומר לכם כשפעולה לא נתמכת.
הבא בתור: Enums
Generics מאפשרים פרמטריזציה על טיפוסים. אבן הבניין הבאה נמצאת בקצה השני של הספקטרום: enums, טיפוס המניה הפשוט של Zero, למקרים שבהם הוריאנטים לא נושאים נתונים נוספים.
שאלות נפוצות
איך generics עובדים ב-Zero?
מצהירים על פרמטרי טיפוס בסוגריים משולשים אחרי שם הפונקציה או ה-shape: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> או shape Pair<T, U> { left: T, right: U }. הקוראים קובעים את הפרמטרים במפורש (Pair<i32, u8>) או נותנים לקומפיילר להסיק אותם מהארגומנטים של הקריאה.
האם shapes יכולים להיות גנריים ב-Zero?
כן. shape יכול לקבל פרמטרי טיפוס באותו תחביר של סוגריים משולשים שמשמש בפונקציות: shape Pair<T, U> { left: T, right: U }. כל שדה יכול להשתמש בפרמטרים בטיפוס שלו. יוצרים מופעים על ידי כתיבת הטיפוס עם הפרמטרים, למשל Pair<i32, u8>.
האם צריך לציין פרמטרי טיפוס כשקוראים לפונקציה גנרית?
בדרך כלל לא. הקומפיילר מסיק אותם מטיפוסי הארגומנטים. מספיק לקרוא ל-makePair(40, 2_u8): T הופך ל-i32 ו-U הופך ל-u8. אפשר לקבוע את הפרמטרים במפורש כשההסקה הייתה בוחרת טיפוס שגוי או כשרוצים שמקום הקריאה יתעד אותם.
מה זה כינוי טיפוס ב-Zero?
כינוי טיפוס הוא שם מקוצר לביטוי טיפוס ארוך יותר. type BytePair = Pair<u8, u8> מאפשר לכתוב BytePair בכל מקום שבו הייתם כותבים Pair<u8, u8>. הכינוי הוא פיצ'ר של שמות בלבד: הוא לא יוצר טיפוס חדש, רק דרך קצרה יותר להתייחס לטיפוס קיים.
איפה generics מופיעים בספרייה הסטנדרטית של Zero?
בכל מקום שבו טיפוס צריך להחזיק טיפוס איבר שרירותי או לפעול עליו. Maybe<T> לערכים אופציונליים, Span<u8> לפרוסות של בתים, טיפוסי מכולות שמקבלים את טיפוס האיבר שלהם כפרמטר. אותו מנגנון גנרי מטפל גם בטיפוסים שהמשתמש מגדיר וגם בטיפוסים של הספרייה הסטנדרטית.