Menu

Generics ב-Golang: פרמטרי טיפוס, אילוצים ודוגמאות

Generics (Go 1.18 ומעלה) מאפשרים לפונקציה או לטיפוס אחד לעבוד עם הרבה טיפוסים ועדיין לשמור על בטיחות טיפוסים. למדו על פרמטרי טיפוס, על האילוצים any, comparable ו-cmp.Ordered, על אילוצים מותאמים עם ~, על טיפוסים גנריים ועל מתי לא להשתמש בהם.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

פונקציה גנרית

פרמטרי טיפוס נכתבים בסוגריים מרובעים לפני הפרמטרים הרגילים. לכל אחד מהם יש אילוץ שאומר אילו טיפוסים מותרים.

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].

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל