פונקציה גנרית
פרמטרי טיפוס נכתבים בסוגריים מרובעים לפני הפרמטרים הרגילים. לכל אחד מהם יש אילוץ שאומר אילו טיפוסים מותרים.
T ו-U מוסקים מהארגומנטים, כך שכמעט אף פעם לא כותבים Map[int, string](...). ההסקה עובדת מתוך הארגומנטים של הפונקציה; היא לא יכולה להסיק טיפוס שמופיע רק בתוצאה. עבור func Zero[T any]() T צריך לכתוב Zero[int]().
לפני Go 1.18 היה צריך לכתוב את הפונקציה הזאת פעם אחת לכל טיפוס, או לקבל []interface{} ולאבד את בטיחות הטיפוסים.
אילוצים
אילוץ הוא interface. הוא מגביל אילו טיפוסים אפשר להשתמש בהם, ובתמורה אומר לקומפיילר אילו פעולות מותרות על ערכים מהטיפוס הזה.
| אילוץ | מאפשר | מה אפשר להשתמש בו |
|---|---|---|
any | כל טיפוס | השמה, העברה, אחסון |
comparable | טיפוסים שתומכים ב-== | ==, !=, מפתחות של map |
cmp.Ordered (Go 1.21) | מספרים שלמים, מספרים עשרוניים, מחרוזות | <, <=, >, >=, ועוד == |
| interface עם מתודות | טיפוסים שיש להם את המתודות האלה | קריאה למתודות |
איחוד טיפוסים כמו ~int | ~float64 | בדיוק הטיפוסים האלה | אופרטורים שכולם תומכים בהם |
עם any במקום comparable, v == target לא מתקמפל: invalid operation: v == target (incomparable types in type set). האילוץ הוא מה שהופך את האופרטור לחוקי.
שתי פונקציות העזר האלה כבר קיימות: slices.Index ו-slices.Max. החבילות הסטנדרטיות slices, maps ו-cmp מכסות את רוב הצרכים הגנריים היומיומיים, אז בדקו אותן לפני שאתם כותבים משלכם.
אילוצים מותאמים עם איחודי טיפוסים ו-~
אילוץ יכול לפרט טיפוסים עם |. האופרטורים המותרים הם אלה שכל הטיפוסים ברשימה תומכים בהם.
הטילדה חשובה. ~int64 פירושו "כל טיפוס שטיפוס הבסיס שלו הוא int64", ולכן הטיפוס בעל השם Cents מתקבל. אם כותבים int64 רגיל, Sum([]Cents{...}) נכשל עם Cents does not satisfy Number (possibly missing ~ for int64 in Number).
interfaces שמכילים איחודי טיפוסים יכולים לשמש רק כאילוצים, אף פעם לא כטיפוסים של משתנים רגילים. var n Number לא מתקמפל.
בחבילה golang.org/x/exp/constraints יש אילוצים מוכנים Integer, Float ו-Signed. היא מחוץ לספרייה הסטנדרטית, ולכן סביבת ההרצה כאן לא יכולה לייבא אותה; cmp.Ordered הוא האילוץ הסטנדרטי.
טיפוסים גנריים
גם ל-structs, slices, maps וטיפוסים אחרים יכולים להיות פרמטרי טיפוס. מתודות על טיפוס גנרי משתמשות בפרמטרים של ה-receiver.
שני פרטים בדוגמה הזאת עולים כל הזמן:
- ערך האפס של T.
var zero Tהיא הדרך להחזיר "כלום" מקוד גנרי. איןT{}אוnilשעובדים לכל טיפוס. - יצירת מופע (instantiation). צריך ליצור מופע של טיפוס גנרי לפני השימוש:
Stack[string], לאStack. בתוך המתודות שלו, ה-receiver נכתבStack[T].
Go 1.24 הוסיפה type aliases גנריים: type Set[T comparable] = map[T]struct{} מתקמפל עכשיו.
מגבלות שתיתקלו בהן
- אין פרמטרי טיפוס במתודות.
func (s *Stack[T]) Map[U any](...)אסור. השתמשו בפונקציה ברמה העליונה. - אין התמחות (specialization). אי אפשר לכתוב מימוש נפרד עבור
T = string. type switch עלany(v)בתוך הפונקציה הוא הפתרון העוקף, ובדרך כלל רמז לכך ש-generics הם הכלי הלא נכון. - אין אילוצי אופרטורים לפי שם. אי אפשר לומר "כל טיפוס עם מתודת
+"; מפרטים את הטיפוסים באיחוד. - גישה לשדות דרך אילוץ לא עובדת. גם אם לכל טיפוס באיחוד יש שדה
ID,v.IDאסור. השתמשו במקום זאת במתודה באילוץ.
מתי לא להשתמש ב-generics
generics מתאימים לטיפוסי מכולה (מחסניות, קבוצות, caches, עצים) ולאלגוריתמים על slices ו-maps שעושים אותו דבר לכל טיפוס איבר. הם לא מתאימים לכל מקום.
- טיפוס קונקרטי אחד. אם קוראים לפונקציה תמיד רק עם
[]User, גרסה גנרית מוסיפה עלות קריאה ותו לא. - התנהגות ששונה לפי טיפוס. בשביל זה יש interfaces. פונקציה שמקבלת
io.Writerפשוטה יותר מפונקציה שמקבלת[T io.Writer], ועובדת אותו דבר. - החלפת פרמטרים מסוג interface.
func Print[T fmt.Stringer](v T)לא טובה יותר מ-func Print(v fmt.Stringer).
ההנחיה של צוות Go עצמו: כתבו קודם את הקוד לטיפוס מסוים, ופנו לפרמטרי טיפוס כשאתם שמים לב שאתם כותבים את אותו קוד בפעם השנייה ורק הטיפוסים השתנו.
טעויות נפוצות
- שימוש ב-
anyבמקום שבו נדרשcomparableאוcmp.Ordered. הקומפיילר דוחה==או<על פרמטר טיפוס מסוגany. - שכחת
~. טיפוסים שהמשתמש הגדיר כמוtype ID intנכשלים באילוץintרגיל. - החזרת
nilעבור פרמטר טיפוס. אסור, אלא אם האילוץ מגביל את T לטיפוסים דמויי מצביע. החזירו ערך אפס. - כתיבת פונקציית עזר גנרית שכבר קיימת. בדקו קודם את
slices,mapsו-cmp.
שאלות נפוצות
האם יש generics ב-Go?
כן, החל מ-Go 1.18 (מרץ 2022). פונקציות וטיפוסים יכולים להצהיר על פרמטרי טיפוס בסוגריים מרובעים: func Map[T, U any](s []T, f func(T) U) []U. לכל פרמטר טיפוס יש אילוץ, שהוא interface שמתאר במה הטיפוס חייב לתמוך.
מה ההבדל בין any ל-comparable ב-generics של Go?
any מאפשר כל טיפוס, אבל כמעט לא מאפשר לעשות כלום עם הערכים חוץ מהשמה, העברה ואחסון. comparable מאפשר רק טיפוסים שתומכים ב-== וב-!=, וזה מה שצריך למפתחות של map או לבדיקות שוויון. עבור < ו->, השתמשו ב-cmp.Ordered.
מה המשמעות של הטילדה (~) באילוץ ב-Go?
~T פירושו "כל טיפוס שטיפוס הבסיס שלו הוא T". ~int מתאים ל-int וגם ל-type Celsius int. בלי הטילדה, int באילוץ מתאים רק ל-int עצמו, ולכן טיפוסים שהמשתמש הגדיר היו נדחים.
האם למתודות ב-Go יכולים להיות פרמטרי טיפוס?
לא. מתודה יכולה להשתמש בפרמטרי הטיפוס של טיפוס ה-receiver שלה (func (s *Stack[T]) Push(v T)), אבל היא לא יכולה להצהיר על חדשים. כתבו במקום זאת פונקציה גנרית ברמה העליונה: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].