perf: Keys() 原生实现 map 分支, 字符串键不再绕 fmt.Sprint
Keys() 的 struct 分支早就是原生的了, map 分支一直委托给 refx, 而那边用的是
fmt.Sprint(k.Interface()) —— 要先把 key 装箱成 interface 再走一遍格式化。
对字符串键完全没必要。
Keys() 3 个字符串键的 map 193.3ns / 9 allocs -> 119.7ns / 5 allocs 1.61x
非字符串键仍旧走 fmt.Sprint: k.String() 对非字符串 Value 返回的是
"<int Value>" 这种占位串而不是键本身, 无脑替换会静默给出错误的键名。
按 m.Type().Key().Kind() 分支处理。
顺带把 normalize() 提到开头, 这样经指针或 interface 拿到的 map/struct 也能
走原生分支, 而不是掉进 slow()。
测试期间发现并修掉一处自己引入的差异: nil map 上旧实现返回 []string{},
我最初加的 IsNil 提前返回给出的是 nil。两者在 reflect.DeepEqual 和 JSON
序列化(`[]` vs `null`)上不同。MapKeys() 对 nil map 本来就返回空切片而不是
panic, 那个提前返回是多余的。
测试覆盖 11 种输入与旧实现对拍(string/具名 string/int/int64/float/bool 键、
空 map、nil map、struct、切片、嵌套 map), 外加经路径取到的 map、
interface 包着的 map、指针指向的 map, 以及非字符串键必须是真实键值的断言。
map 迭代顺序随机, 比较前排序。
This commit is contained in:
@@ -651,13 +651,42 @@ func (v *rfx) Keys() []string {
|
|||||||
if !v.valid() {
|
if !v.valid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if v.td.Kind == reflect.Struct {
|
cur, ok := (*v).normalize()
|
||||||
out := make([]string, len(v.td.fields))
|
if !ok {
|
||||||
for i := range v.td.fields {
|
return nil
|
||||||
out[i] = v.td.fields[i].Name
|
}
|
||||||
|
|
||||||
|
switch cur.td.Kind {
|
||||||
|
case reflect.Struct:
|
||||||
|
out := make([]string, len(cur.td.fields))
|
||||||
|
for i := range cur.td.fields {
|
||||||
|
out[i] = cur.td.fields[i].Name
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
// 不必判 IsNil: MapKeys() 对 nil map 返回空切片而不是 panic,
|
||||||
|
// 于是这里得到 []string{} —— 与旧实现一致。提前返回 nil 会让
|
||||||
|
// DeepEqual 和 JSON 序列化(`[]` vs `null`)出现差异。
|
||||||
|
m := valueAt(cur.td, cur.ptr)
|
||||||
|
keys := m.MapKeys()
|
||||||
|
out := make([]string, len(keys))
|
||||||
|
// 字符串键直接取, 不必绕 fmt.Sprint —— 后者要先把 key 装箱成
|
||||||
|
// interface 再走一遍格式化, 实测 3 个键就差一倍。
|
||||||
|
// 非字符串键仍旧交给 fmt.Sprint: k.String() 对非字符串 Value 返回的是
|
||||||
|
// "<int Value>" 这种占位串, 不是键本身。
|
||||||
|
if m.Type().Key().Kind() == reflect.String {
|
||||||
|
for i, k := range keys {
|
||||||
|
out[i] = k.String()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i, k := range keys {
|
||||||
|
out[i] = fmt.Sprint(k.Interface())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
return v.slow().Keys()
|
return v.slow().Keys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package reflux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keys() 的 map 分支现在按键类型分支: 字符串键直接取, 其余仍走 fmt.Sprint。
|
||||||
|
// 这一组守的是"分支之后行为不变" —— 尤其是非字符串键不能退化成
|
||||||
|
// k.String() 的 "<int Value>" 占位串。
|
||||||
|
|
||||||
|
type keyStr string
|
||||||
|
|
||||||
|
func TestKeysMatchesLegacy(t *testing.T) {
|
||||||
|
type inner struct{ A, B string }
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
val func() any
|
||||||
|
}{
|
||||||
|
{"string 键 map", func() any { return &map[string]int{"b": 2, "a": 1, "c": 3} }},
|
||||||
|
{"具名 string 键 map", func() any { return &map[keyStr]int{"b": 2, "a": 1} }},
|
||||||
|
{"int 键 map", func() any { return &map[int]string{2: "b", 1: "a", 10: "j"} }},
|
||||||
|
{"int64 键 map", func() any { return &map[int64]string{7: "x"} }},
|
||||||
|
{"float 键 map", func() any { return &map[float64]string{1.5: "x"} }},
|
||||||
|
{"bool 键 map", func() any { return &map[bool]string{true: "t", false: "f"} }},
|
||||||
|
{"空 map", func() any { return &map[string]int{} }},
|
||||||
|
{"nil map", func() any { var m map[string]int; return &m }},
|
||||||
|
{"struct", func() any { return &inner{} }},
|
||||||
|
{"切片(两边都不支持)", func() any { return &[]string{"a"} }},
|
||||||
|
{"map 里的 map", func() any { return &map[string]any{"sub": map[string]any{"x": 1}} }},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
want := newLegacy(c.val()).Keys()
|
||||||
|
got := New(c.val()).Keys()
|
||||||
|
// map 迭代顺序随机, 比较前排序
|
||||||
|
sort.Strings(want)
|
||||||
|
sort.Strings(got)
|
||||||
|
if !reflect.DeepEqual(want, got) {
|
||||||
|
t.Fatalf("Keys() 不一致\n 旧 = %#v\n 新 = %#v", want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 经路径取到的 map / interface 包着的 map 也要正确
|
||||||
|
func TestKeysThroughPath(t *testing.T) {
|
||||||
|
doc := &struct {
|
||||||
|
Meta map[string]string
|
||||||
|
Any any
|
||||||
|
Ptr *map[string]string
|
||||||
|
}{
|
||||||
|
Meta: map[string]string{"k1": "v", "k2": "v"},
|
||||||
|
Any: map[string]string{"a1": "v"},
|
||||||
|
}
|
||||||
|
m := map[string]string{"p1": "v"}
|
||||||
|
doc.Ptr = &m
|
||||||
|
|
||||||
|
r, l := New(doc), newLegacy(doc)
|
||||||
|
for _, p := range []string{"Meta", "Any", "Ptr"} {
|
||||||
|
t.Run(p, func(t *testing.T) {
|
||||||
|
want, got := l.Get(p).Keys(), r.Get(p).Keys()
|
||||||
|
sort.Strings(want)
|
||||||
|
sort.Strings(got)
|
||||||
|
if !reflect.DeepEqual(want, got) {
|
||||||
|
t.Fatalf("Get(%q).Keys() 不一致\n 旧 = %#v\n 新 = %#v", p, want, got)
|
||||||
|
}
|
||||||
|
if len(got) == 0 {
|
||||||
|
t.Fatalf("Get(%q).Keys() 不该为空", p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非字符串键必须是键本身的值, 不能是 "<int Value>" 这种占位串
|
||||||
|
func TestKeysNonStringKeysAreReal(t *testing.T) {
|
||||||
|
got := New(&map[int]string{42: "x"}).Keys()
|
||||||
|
if len(got) != 1 || got[0] != "42" {
|
||||||
|
t.Fatalf("int 键的 Keys() = %#v, 期望 [\"42\"]", got)
|
||||||
|
}
|
||||||
|
if got := New(&map[bool]string{true: "x"}).Keys(); len(got) != 1 || got[0] != "true" {
|
||||||
|
t.Fatalf("bool 键的 Keys() = %#v, 期望 [\"true\"]", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user