הניב: map עם ערכים ריקים
ל-Go אין מילת מפתח set ואין set בספרייה הסטנדרטית. map שהמפתחות שלו הם האיברים והערכים שלו לא נושאים כלום עושה את העבודה.
struct{} הוא טיפוס ה-struct הריק ו-struct{}{} הוא הערך היחיד שלו. הוא תופס אפס בתים, כך שה-map שומר רק מפתחות.
כל הכללים של map חלים כאן: המפתחות חייבים להיות comparable, סדר המעבר אקראי, map שהוא nil גורם ל-panic בכתיבה, וכתיבות מקבילות דורשות נעילה. הדף על maps מכסה כל אחד מהם.
map[T]struct{} או map[T]bool
הכתיב הנפוץ השני הוא map[T]bool. הוא נקרא טוב יותר כי מפתח חסר מחזיר false:
seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }
השיקולים:
map[T]struct{} | map[T]bool | |
|---|---|---|
| גודל ערך | 0 בתים | בית אחד (ועוד יישור) |
| בדיקת שייכות | _, ok := s[k] | s[k] |
| דו משמעות | אין | s[k] = false הוא מצב שלישי |
השורה השלישית היא הסיבה האמיתית שבהרבה בסיסי קוד מעדיפים את ה-struct הריק: עם bool, מישהו בסוף כותב s[k] = false ו-len(s) מפסיק להיות מספר האיברים. ב-sets קטנים ההבדל בזיכרון לא משנה.
הסרת כפילויות מ-slice
השימוש הנפוץ ביותר ב-set הוא הסרת כפילויות. הקוד הזה שומר את ההופעה הראשונה של כל ערך ומשמר את הסדר:
פלט:
[b a c]
[3 1 2]
[1 2 3]
איחוד, חיתוך, הפרש
אלגברה של sets היא כמה לולאות. עברו על ה-set הקטן יותר כשבודקים שייכות בשני, כי כל חיפוש לוקח בממוצע זמן קבוע.
sorted קיימת רק כדי שהפלט יהיה יציב. הדפסת set באמצעות מעבר עליו עם range נותנת סדר שונה בכל הרצה. maps.Keys ו-slices.Sorted דורשות Go 1.23.
טיפוס עם שם כמו type set map[string]struct{} הוא עדיין map: ניגשים אליו לפי מפתח, עוברים עליו עם range ומוחקים ממנו עם delete באותה צורה, ואפשר להצמיד לו מתודות.
Set גנרי קטן
עם generics (Go 1.18), טיפוס אחד מכסה כל טיפוס איבר שהוא comparable. זה מספיק לרוב התוכניות:
עטיפת ה-map ב-struct מסתירה את הרעש של struct{}{} ומבטיחה שה-map נוצר בבנאי, וזה מבטל את ה-panic של map שהוא nil. Sorted לא יכולה להיות מתודה: מתודה לא יכולה להצהיר על פרמטרי טיפוס משלה או להדק את האילוץ comparable של הטיפוס, ומיון דורש cmp.Ordered. הדף על generics מסביר אילוצים.
אם צריך set עם כל היכולות (גרסאות בטוחות ל-threads, הרבה פעולות), יש חבילות צד שלישי כמו github.com/deckarep/golang-set. ברוב הקוד, מתכנתי Go משתמשים בניב של map או בטיפוס של 30 שורות כמו זה.
Sets של structs
כל טיפוס comparable יכול להיות איבר, כולל structs עם שדות comparable. זה הופך בדיקות של "האם כבר ראיתי את הזוג הזה" לישירות:
type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}
slices ו-maps לא יכולים להיות איברים של set. כדי לעקוב אחרי slices ייחודיים, המירו קודם כל אחד למפתח comparable, למשל מערך בגודל קבוע, או מחרוזת שנבנתה עם fmt.Sprint.
טעויות נפוצות
- שכחה לאתחל.
var s map[string]struct{}הוא nil; ההוספה הראשונה גורמת ל-panic. - הדפסת set וציפייה לפלט יציב. מיינו את האיברים קודם.
- שימוש ב-
map[T]boolושמירתfalse. אזlenכבר לא סופרת איברים. השתמשו ב-deleteכדי להסיר.
שאלות נפוצות
האם יש ל-Go טיפוס set?
לא. בספרייה הסטנדרטית אין set. הניב המקובל הוא map שהערכים שלו לא נושאים מידע: map[string]struct{}. הוספה היא s[k] = struct{}{}, בדיקת שייכות היא _, ok := s[k], הסרה היא delete(s, k), והגודל הוא len(s).
האם להשתמש ב-map[T]bool או ב-map[T]struct{} בתור set ב-Go?
map[T]struct{} מבהיר את הכוונה והערכים שלו תופסים אפס בתים. map[T]bool נקרא בצורה טבעית יותר (if seen[x]) כי מפתח חסר מחזיר false. שניהם נכונים; ההבדל בזיכרון משנה רק ב-sets גדולים מאוד. בחרו אחד והיו עקביים.
איך מסירים כפילויות מ-slice ב-Go?
כדי לשמור את ההופעה הראשונה לפי הסדר, עברו בלולאה ועקבו אחרי ערכים שכבר נראו ב-map[T]struct{}, והוסיפו רק ערכים שעוד לא נראו. אם הסדר לא משנה, מיינו והסירו כפילויות סמוכות: slices.Sort(s); s = slices.Compact(s).