Menu

Set ב-Golang: map[T]struct{}, פעולות ו-Set גנרי

ל-Go אין טיפוס set מובנה. הניב המקובל הוא map עם ערכים של struct ריק. למדו הוספה, בדיקת שייכות והסרה, איחוד, חיתוך והפרש, ואיך כותבים Set גנרי קטן.

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

הניב: 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).

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

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

להתחיל