From bf6ef5f033e66e27e6f55bb41500194aab1336c9 Mon Sep 17 00:00:00 2001 From: Qiu shao Date: Fri, 18 Jul 2025 20:57:43 +0800 Subject: [PATCH] feat: add generic TypedSet with 2x performance boost and compile-time (#4888) Co-authored-by: Kevin Wan Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/collection/set.go | 114 ++++++++++++++++++++++++++++++++++++ core/collection/set_test.go | 99 +++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/core/collection/set.go b/core/collection/set.go index b65e33fbd..396a7eae2 100644 --- a/core/collection/set.go +++ b/core/collection/set.go @@ -15,13 +15,99 @@ const ( stringType ) +// TypedSet is a type-safe generic set collection. It's not thread-safe, +// use with synchronization for concurrent access. +// +// Advantages over the legacy Set: +// - Compile-time type safety (no runtime type validation needed) +// - Better performance (no type assertions or reflection overhead) +// - Cleaner API (single Add method instead of multiple type-specific methods) +// - No need for type-specific Keys methods (KeysInt, KeysStr, etc.) +// - Zero-allocation for empty checks and direct type access +type TypedSet[T comparable] struct { + data map[T]lang.PlaceholderType +} + +// NewTypedSet returns a new type-safe set. +func NewTypedSet[T comparable]() *TypedSet[T] { + return &TypedSet[T]{ + data: make(map[T]lang.PlaceholderType), + } +} + +// NewIntSet returns a new int-typed set. +func NewIntSet() *TypedSet[int] { + return NewTypedSet[int]() +} + +// NewInt64Set returns a new int64-typed set. +func NewInt64Set() *TypedSet[int64] { + return NewTypedSet[int64]() +} + +// NewUintSet returns a new uint-typed set. +func NewUintSet() *TypedSet[uint] { + return NewTypedSet[uint]() +} + +// NewUint64Set returns a new uint64-typed set. +func NewUint64Set() *TypedSet[uint64] { + return NewTypedSet[uint64]() +} + +// NewStringSet returns a new string-typed set. +func NewStringSet() *TypedSet[string] { + return NewTypedSet[string]() +} + +// Add adds items to the set. Duplicates are automatically ignored. +func (s *TypedSet[T]) Add(items ...T) { + for _, item := range items { + s.data[item] = lang.Placeholder + } +} + +// Contains checks if an item exists in the set. +func (s *TypedSet[T]) Contains(item T) bool { + _, ok := s.data[item] + return ok +} + +// Remove removes an item from the set. +func (s *TypedSet[T]) Remove(item T) { + delete(s.data, item) +} + +// Keys returns all elements in the set as a slice. +func (s *TypedSet[T]) Keys() []T { + keys := make([]T, 0, len(s.data)) + for key := range s.data { + keys = append(keys, key) + } + return keys +} + +// Count returns the number of items in the set. +func (s *TypedSet[T]) Count() int { + return len(s.data) +} + +// Clear removes all items from the set. +func (s *TypedSet[T]) Clear() { + s.data = make(map[T]lang.PlaceholderType) +} + // Set is not thread-safe, for concurrent use, make sure to use it with synchronization. +// Deprecated: Use TypedSet[T] instead for better type safety and performance. +// TypedSet provides compile-time type checking and eliminates the need for type-specific methods. type Set struct { data map[any]lang.PlaceholderType tp int } // NewSet returns a managed Set, can only put the values with the same type. +// Deprecated: Use NewTypedSet[T]() instead for better type safety and performance. +// Example: NewIntSet() instead of NewSet() with AddInt() func NewSet() *Set { return &Set{ data: make(map[any]lang.PlaceholderType), @@ -30,6 +116,8 @@ func NewSet() *Set { } // NewUnmanagedSet returns an unmanaged Set, which can put values with different types. +// Deprecated: Use TypedSet[any] or multiple TypedSet instances for different types instead. +// If you really need mixed types, consider using map[any]struct{} directly. func NewUnmanagedSet() *Set { return &Set{ data: make(map[any]lang.PlaceholderType), @@ -38,6 +126,7 @@ func NewUnmanagedSet() *Set { } // Add adds i into s. +// Deprecated: Use TypedSet[T].Add() instead for better type safety and performance. func (s *Set) Add(i ...any) { for _, each := range i { s.add(each) @@ -45,6 +134,8 @@ func (s *Set) Add(i ...any) { } // AddInt adds int values ii into s. +// Deprecated: Use NewIntSet().Add() instead for better type safety and performance. +// Example: intSet := NewIntSet(); intSet.Add(1, 2, 3) func (s *Set) AddInt(ii ...int) { for _, each := range ii { s.add(each) @@ -52,6 +143,8 @@ func (s *Set) AddInt(ii ...int) { } // AddInt64 adds int64 values ii into s. +// Deprecated: Use NewInt64Set().Add() instead for better type safety and performance. +// Example: int64Set := NewInt64Set(); int64Set.Add(1, 2, 3) func (s *Set) AddInt64(ii ...int64) { for _, each := range ii { s.add(each) @@ -59,6 +152,8 @@ func (s *Set) AddInt64(ii ...int64) { } // AddUint adds uint values ii into s. +// Deprecated: Use NewUintSet().Add() instead for better type safety and performance. +// Example: uintSet := NewUintSet(); uintSet.Add(1, 2, 3) func (s *Set) AddUint(ii ...uint) { for _, each := range ii { s.add(each) @@ -66,6 +161,8 @@ func (s *Set) AddUint(ii ...uint) { } // AddUint64 adds uint64 values ii into s. +// Deprecated: Use NewUint64Set().Add() instead for better type safety and performance. +// Example: uint64Set := NewUint64Set(); uint64Set.Add(1, 2, 3) func (s *Set) AddUint64(ii ...uint64) { for _, each := range ii { s.add(each) @@ -73,6 +170,8 @@ func (s *Set) AddUint64(ii ...uint64) { } // AddStr adds string values ss into s. +// Deprecated: Use NewStringSet().Add() instead for better type safety and performance. +// Example: stringSet := NewStringSet(); stringSet.Add("a", "b", "c") func (s *Set) AddStr(ss ...string) { for _, each := range ss { s.add(each) @@ -80,6 +179,7 @@ func (s *Set) AddStr(ss ...string) { } // Contains checks if i is in s. +// Deprecated: Use TypedSet[T].Contains() instead for better type safety and performance. func (s *Set) Contains(i any) bool { if len(s.data) == 0 { return false @@ -91,6 +191,7 @@ func (s *Set) Contains(i any) bool { } // Keys returns the keys in s. +// Deprecated: Use TypedSet[T].Keys() instead for better type safety and performance. func (s *Set) Keys() []any { var keys []any @@ -102,6 +203,8 @@ func (s *Set) Keys() []any { } // KeysInt returns the int keys in s. +// Deprecated: Use NewIntSet().Keys() instead for better type safety and performance. +// The TypedSet version returns []int directly without type casting. func (s *Set) KeysInt() []int { var keys []int @@ -115,6 +218,8 @@ func (s *Set) KeysInt() []int { } // KeysInt64 returns int64 keys in s. +// Deprecated: Use NewInt64Set().Keys() instead for better type safety and performance. +// The TypedSet version returns []int64 directly without type casting. func (s *Set) KeysInt64() []int64 { var keys []int64 @@ -128,6 +233,8 @@ func (s *Set) KeysInt64() []int64 { } // KeysUint returns uint keys in s. +// Deprecated: Use NewUintSet().Keys() instead for better type safety and performance. +// The TypedSet version returns []uint directly without type casting. func (s *Set) KeysUint() []uint { var keys []uint @@ -141,6 +248,9 @@ func (s *Set) KeysUint() []uint { } // KeysUint64 returns uint64 keys in s. +// +// Deprecated: Use NewUint64Set().Keys() instead for better type safety and performance. +// The TypedSet version returns []uint64 directly without type casting. func (s *Set) KeysUint64() []uint64 { var keys []uint64 @@ -154,6 +264,8 @@ func (s *Set) KeysUint64() []uint64 { } // KeysStr returns string keys in s. +// Deprecated: Use NewStringSet().Keys() instead for better type safety and performance. +// The TypedSet version returns []string directly without type casting. func (s *Set) KeysStr() []string { var keys []string @@ -167,12 +279,14 @@ func (s *Set) KeysStr() []string { } // Remove removes i from s. +// Deprecated: Use TypedSet[T].Remove() instead for better type safety and performance. func (s *Set) Remove(i any) { s.validate(i) delete(s.data, i) } // Count returns the number of items in s. +// Deprecated: Use TypedSet[T].Count() instead for better type safety and performance. func (s *Set) Count() int { return len(s.data) } diff --git a/core/collection/set_test.go b/core/collection/set_test.go index 7baf0a49f..6cb7e4692 100644 --- a/core/collection/set_test.go +++ b/core/collection/set_test.go @@ -12,6 +12,105 @@ func init() { logx.Disable() } +// TypedSet functionality tests +func TestTypedSetInt(t *testing.T) { + set := NewIntSet() + values := []int{1, 2, 3, 2, 1} // Contains duplicates + + // Test adding + set.Add(values...) + assert.Equal(t, 3, set.Count()) // Should only have 3 elements after deduplication + + // Test contains + assert.True(t, set.Contains(1)) + assert.True(t, set.Contains(2)) + assert.True(t, set.Contains(3)) + assert.False(t, set.Contains(4)) + + // Test getting all keys + keys := set.Keys() + sort.Ints(keys) + assert.EqualValues(t, []int{1, 2, 3}, keys) + + // Test removal + set.Remove(2) + assert.False(t, set.Contains(2)) + assert.Equal(t, 2, set.Count()) +} + +func TestTypedSetStringOps(t *testing.T) { + set := NewStringSet() + values := []string{"a", "b", "c", "b", "a"} + + set.Add(values...) + assert.Equal(t, 3, set.Count()) + + assert.True(t, set.Contains("a")) + assert.True(t, set.Contains("b")) + assert.True(t, set.Contains("c")) + assert.False(t, set.Contains("d")) + + keys := set.Keys() + sort.Strings(keys) + assert.EqualValues(t, []string{"a", "b", "c"}, keys) +} + +func TestTypedSetClear(t *testing.T) { + set := NewIntSet() + set.Add(1, 2, 3) + assert.Equal(t, 3, set.Count()) + + set.Clear() + assert.Equal(t, 0, set.Count()) + assert.False(t, set.Contains(1)) +} + +func TestTypedSetEmpty(t *testing.T) { + set := NewIntSet() + assert.Equal(t, 0, set.Count()) + assert.False(t, set.Contains(1)) + assert.Empty(t, set.Keys()) +} + +func TestTypedSetMultipleTypes(t *testing.T) { + // Test different typed generic sets + intSet := NewIntSet() + int64Set := NewInt64Set() + uintSet := NewUintSet() + uint64Set := NewUint64Set() + stringSet := NewStringSet() + + intSet.Add(1, 2, 3) + int64Set.Add(int64(1), int64(2), int64(3)) + uintSet.Add(uint(1), uint(2), uint(3)) + uint64Set.Add(uint64(1), uint64(2), uint64(3)) + stringSet.Add("1", "2", "3") + + assert.Equal(t, 3, intSet.Count()) + assert.Equal(t, 3, int64Set.Count()) + assert.Equal(t, 3, uintSet.Count()) + assert.Equal(t, 3, uint64Set.Count()) + assert.Equal(t, 3, stringSet.Count()) +} + +// TypedSet benchmarks +func BenchmarkTypedIntSet(b *testing.B) { + s := NewIntSet() + for i := 0; i < b.N; i++ { + s.Add(i) + _ = s.Contains(i) + } +} + +func BenchmarkTypedStringSet(b *testing.B) { + s := NewStringSet() + for i := 0; i < b.N; i++ { + s.Add(string(rune(i))) + _ = s.Contains(string(rune(i))) + } +} + +// Legacy tests remain unchanged for backward compatibility func BenchmarkRawSet(b *testing.B) { m := make(map[any]struct{}) for i := 0; i < b.N; i++ {