Skip to content

Commit bf6ef5f

Browse files
codeErrorSleepkevwanCopilot
authored
feat: add generic TypedSet with 2x performance boost and compile-time (#4888)
Co-authored-by: Kevin Wan <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent ff89062 commit bf6ef5f

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed

core/collection/set.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,99 @@ const (
1515
stringType
1616
)
1717

18+
// TypedSet is a type-safe generic set collection. It's not thread-safe,
19+
// use with synchronization for concurrent access.
20+
//
21+
// Advantages over the legacy Set:
22+
// - Compile-time type safety (no runtime type validation needed)
23+
// - Better performance (no type assertions or reflection overhead)
24+
// - Cleaner API (single Add method instead of multiple type-specific methods)
25+
// - No need for type-specific Keys methods (KeysInt, KeysStr, etc.)
26+
// - Zero-allocation for empty checks and direct type access
27+
type TypedSet[T comparable] struct {
28+
data map[T]lang.PlaceholderType
29+
}
30+
31+
// NewTypedSet returns a new type-safe set.
32+
func NewTypedSet[T comparable]() *TypedSet[T] {
33+
return &TypedSet[T]{
34+
data: make(map[T]lang.PlaceholderType),
35+
}
36+
}
37+
38+
// NewIntSet returns a new int-typed set.
39+
func NewIntSet() *TypedSet[int] {
40+
return NewTypedSet[int]()
41+
}
42+
43+
// NewInt64Set returns a new int64-typed set.
44+
func NewInt64Set() *TypedSet[int64] {
45+
return NewTypedSet[int64]()
46+
}
47+
48+
// NewUintSet returns a new uint-typed set.
49+
func NewUintSet() *TypedSet[uint] {
50+
return NewTypedSet[uint]()
51+
}
52+
53+
// NewUint64Set returns a new uint64-typed set.
54+
func NewUint64Set() *TypedSet[uint64] {
55+
return NewTypedSet[uint64]()
56+
}
57+
58+
// NewStringSet returns a new string-typed set.
59+
func NewStringSet() *TypedSet[string] {
60+
return NewTypedSet[string]()
61+
}
62+
63+
// Add adds items to the set. Duplicates are automatically ignored.
64+
func (s *TypedSet[T]) Add(items ...T) {
65+
for _, item := range items {
66+
s.data[item] = lang.Placeholder
67+
}
68+
}
69+
70+
// Contains checks if an item exists in the set.
71+
func (s *TypedSet[T]) Contains(item T) bool {
72+
_, ok := s.data[item]
73+
return ok
74+
}
75+
76+
// Remove removes an item from the set.
77+
func (s *TypedSet[T]) Remove(item T) {
78+
delete(s.data, item)
79+
}
80+
81+
// Keys returns all elements in the set as a slice.
82+
func (s *TypedSet[T]) Keys() []T {
83+
keys := make([]T, 0, len(s.data))
84+
for key := range s.data {
85+
keys = append(keys, key)
86+
}
87+
return keys
88+
}
89+
90+
// Count returns the number of items in the set.
91+
func (s *TypedSet[T]) Count() int {
92+
return len(s.data)
93+
}
94+
95+
// Clear removes all items from the set.
96+
func (s *TypedSet[T]) Clear() {
97+
s.data = make(map[T]lang.PlaceholderType)
98+
}
99+
18100
// Set is not thread-safe, for concurrent use, make sure to use it with synchronization.
101+
// Deprecated: Use TypedSet[T] instead for better type safety and performance.
102+
// TypedSet provides compile-time type checking and eliminates the need for type-specific methods.
19103
type Set struct {
20104
data map[any]lang.PlaceholderType
21105
tp int
22106
}
23107

24108
// NewSet returns a managed Set, can only put the values with the same type.
109+
// Deprecated: Use NewTypedSet[T]() instead for better type safety and performance.
110+
// Example: NewIntSet() instead of NewSet() with AddInt()
25111
func NewSet() *Set {
26112
return &Set{
27113
data: make(map[any]lang.PlaceholderType),
@@ -30,6 +116,8 @@ func NewSet() *Set {
30116
}
31117

32118
// NewUnmanagedSet returns an unmanaged Set, which can put values with different types.
119+
// Deprecated: Use TypedSet[any] or multiple TypedSet instances for different types instead.
120+
// If you really need mixed types, consider using map[any]struct{} directly.
33121
func NewUnmanagedSet() *Set {
34122
return &Set{
35123
data: make(map[any]lang.PlaceholderType),
@@ -38,48 +126,60 @@ func NewUnmanagedSet() *Set {
38126
}
39127

40128
// Add adds i into s.
129+
// Deprecated: Use TypedSet[T].Add() instead for better type safety and performance.
41130
func (s *Set) Add(i ...any) {
42131
for _, each := range i {
43132
s.add(each)
44133
}
45134
}
46135

47136
// AddInt adds int values ii into s.
137+
// Deprecated: Use NewIntSet().Add() instead for better type safety and performance.
138+
// Example: intSet := NewIntSet(); intSet.Add(1, 2, 3)
48139
func (s *Set) AddInt(ii ...int) {
49140
for _, each := range ii {
50141
s.add(each)
51142
}
52143
}
53144

54145
// AddInt64 adds int64 values ii into s.
146+
// Deprecated: Use NewInt64Set().Add() instead for better type safety and performance.
147+
// Example: int64Set := NewInt64Set(); int64Set.Add(1, 2, 3)
55148
func (s *Set) AddInt64(ii ...int64) {
56149
for _, each := range ii {
57150
s.add(each)
58151
}
59152
}
60153

61154
// AddUint adds uint values ii into s.
155+
// Deprecated: Use NewUintSet().Add() instead for better type safety and performance.
156+
// Example: uintSet := NewUintSet(); uintSet.Add(1, 2, 3)
62157
func (s *Set) AddUint(ii ...uint) {
63158
for _, each := range ii {
64159
s.add(each)
65160
}
66161
}
67162

68163
// AddUint64 adds uint64 values ii into s.
164+
// Deprecated: Use NewUint64Set().Add() instead for better type safety and performance.
165+
// Example: uint64Set := NewUint64Set(); uint64Set.Add(1, 2, 3)
69166
func (s *Set) AddUint64(ii ...uint64) {
70167
for _, each := range ii {
71168
s.add(each)
72169
}
73170
}
74171

75172
// AddStr adds string values ss into s.
173+
// Deprecated: Use NewStringSet().Add() instead for better type safety and performance.
174+
// Example: stringSet := NewStringSet(); stringSet.Add("a", "b", "c")
76175
func (s *Set) AddStr(ss ...string) {
77176
for _, each := range ss {
78177
s.add(each)
79178
}
80179
}
81180

82181
// Contains checks if i is in s.
182+
// Deprecated: Use TypedSet[T].Contains() instead for better type safety and performance.
83183
func (s *Set) Contains(i any) bool {
84184
if len(s.data) == 0 {
85185
return false
@@ -91,6 +191,7 @@ func (s *Set) Contains(i any) bool {
91191
}
92192

93193
// Keys returns the keys in s.
194+
// Deprecated: Use TypedSet[T].Keys() instead for better type safety and performance.
94195
func (s *Set) Keys() []any {
95196
var keys []any
96197

@@ -102,6 +203,8 @@ func (s *Set) Keys() []any {
102203
}
103204

104205
// KeysInt returns the int keys in s.
206+
// Deprecated: Use NewIntSet().Keys() instead for better type safety and performance.
207+
// The TypedSet version returns []int directly without type casting.
105208
func (s *Set) KeysInt() []int {
106209
var keys []int
107210

@@ -115,6 +218,8 @@ func (s *Set) KeysInt() []int {
115218
}
116219

117220
// KeysInt64 returns int64 keys in s.
221+
// Deprecated: Use NewInt64Set().Keys() instead for better type safety and performance.
222+
// The TypedSet version returns []int64 directly without type casting.
118223
func (s *Set) KeysInt64() []int64 {
119224
var keys []int64
120225

@@ -128,6 +233,8 @@ func (s *Set) KeysInt64() []int64 {
128233
}
129234

130235
// KeysUint returns uint keys in s.
236+
// Deprecated: Use NewUintSet().Keys() instead for better type safety and performance.
237+
// The TypedSet version returns []uint directly without type casting.
131238
func (s *Set) KeysUint() []uint {
132239
var keys []uint
133240

@@ -141,6 +248,9 @@ func (s *Set) KeysUint() []uint {
141248
}
142249

143250
// KeysUint64 returns uint64 keys in s.
251+
//
252+
// Deprecated: Use NewUint64Set().Keys() instead for better type safety and performance.
253+
// The TypedSet version returns []uint64 directly without type casting.
144254
func (s *Set) KeysUint64() []uint64 {
145255
var keys []uint64
146256

@@ -154,6 +264,8 @@ func (s *Set) KeysUint64() []uint64 {
154264
}
155265

156266
// KeysStr returns string keys in s.
267+
// Deprecated: Use NewStringSet().Keys() instead for better type safety and performance.
268+
// The TypedSet version returns []string directly without type casting.
157269
func (s *Set) KeysStr() []string {
158270
var keys []string
159271

@@ -167,12 +279,14 @@ func (s *Set) KeysStr() []string {
167279
}
168280

169281
// Remove removes i from s.
282+
// Deprecated: Use TypedSet[T].Remove() instead for better type safety and performance.
170283
func (s *Set) Remove(i any) {
171284
s.validate(i)
172285
delete(s.data, i)
173286
}
174287

175288
// Count returns the number of items in s.
289+
// Deprecated: Use TypedSet[T].Count() instead for better type safety and performance.
176290
func (s *Set) Count() int {
177291
return len(s.data)
178292
}

core/collection/set_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,105 @@ func init() {
1212
logx.Disable()
1313
}
1414

15+
// TypedSet functionality tests
16+
func TestTypedSetInt(t *testing.T) {
17+
set := NewIntSet()
18+
values := []int{1, 2, 3, 2, 1} // Contains duplicates
19+
20+
// Test adding
21+
set.Add(values...)
22+
assert.Equal(t, 3, set.Count()) // Should only have 3 elements after deduplication
23+
24+
// Test contains
25+
assert.True(t, set.Contains(1))
26+
assert.True(t, set.Contains(2))
27+
assert.True(t, set.Contains(3))
28+
assert.False(t, set.Contains(4))
29+
30+
// Test getting all keys
31+
keys := set.Keys()
32+
sort.Ints(keys)
33+
assert.EqualValues(t, []int{1, 2, 3}, keys)
34+
35+
// Test removal
36+
set.Remove(2)
37+
assert.False(t, set.Contains(2))
38+
assert.Equal(t, 2, set.Count())
39+
}
40+
41+
func TestTypedSetStringOps(t *testing.T) {
42+
set := NewStringSet()
43+
values := []string{"a", "b", "c", "b", "a"}
44+
45+
set.Add(values...)
46+
assert.Equal(t, 3, set.Count())
47+
48+
assert.True(t, set.Contains("a"))
49+
assert.True(t, set.Contains("b"))
50+
assert.True(t, set.Contains("c"))
51+
assert.False(t, set.Contains("d"))
52+
53+
keys := set.Keys()
54+
sort.Strings(keys)
55+
assert.EqualValues(t, []string{"a", "b", "c"}, keys)
56+
}
57+
58+
func TestTypedSetClear(t *testing.T) {
59+
set := NewIntSet()
60+
set.Add(1, 2, 3)
61+
assert.Equal(t, 3, set.Count())
62+
63+
set.Clear()
64+
assert.Equal(t, 0, set.Count())
65+
assert.False(t, set.Contains(1))
66+
}
67+
68+
func TestTypedSetEmpty(t *testing.T) {
69+
set := NewIntSet()
70+
assert.Equal(t, 0, set.Count())
71+
assert.False(t, set.Contains(1))
72+
assert.Empty(t, set.Keys())
73+
}
74+
75+
func TestTypedSetMultipleTypes(t *testing.T) {
76+
// Test different typed generic sets
77+
intSet := NewIntSet()
78+
int64Set := NewInt64Set()
79+
uintSet := NewUintSet()
80+
uint64Set := NewUint64Set()
81+
stringSet := NewStringSet()
82+
83+
intSet.Add(1, 2, 3)
84+
int64Set.Add(int64(1), int64(2), int64(3))
85+
uintSet.Add(uint(1), uint(2), uint(3))
86+
uint64Set.Add(uint64(1), uint64(2), uint64(3))
87+
stringSet.Add("1", "2", "3")
88+
89+
assert.Equal(t, 3, intSet.Count())
90+
assert.Equal(t, 3, int64Set.Count())
91+
assert.Equal(t, 3, uintSet.Count())
92+
assert.Equal(t, 3, uint64Set.Count())
93+
assert.Equal(t, 3, stringSet.Count())
94+
}
95+
96+
// TypedSet benchmarks
97+
func BenchmarkTypedIntSet(b *testing.B) {
98+
s := NewIntSet()
99+
for i := 0; i < b.N; i++ {
100+
s.Add(i)
101+
_ = s.Contains(i)
102+
}
103+
}
104+
105+
func BenchmarkTypedStringSet(b *testing.B) {
106+
s := NewStringSet()
107+
for i := 0; i < b.N; i++ {
108+
s.Add(string(rune(i)))
109+
_ = s.Contains(string(rune(i)))
110+
}
111+
}
112+
113+
// Legacy tests remain unchanged for backward compatibility
15114
func BenchmarkRawSet(b *testing.B) {
16115
m := make(map[any]struct{})
17116
for i := 0; i < b.N; i++ {

0 commit comments

Comments
 (0)