package reflux // 泛型 Get[T] 的等价性与分配情况。 // // 分配这块用 testing.AllocsPerRun 精确统计, 并且必须写进包级 sink 变量 —— // 丢弃结果的写法会被编译器优化掉, 测出假的 0 分配。 import ( "fmt" "testing" ) // 泛型 Get[T] 必须与 r.Get(path...).Xxx() 等价, 且在"非 *rfx 实现的 R"上能正确回退。 func TestGenericGetEquivalent(t *testing.T) { box := newConvBox() fast := New(box) // 走快路径 slow := newLegacy(box) // 走回退路径 for _, field := range convFields { t.Run(field, func(t *testing.T) { // string want, wp := callSafe(slow.Get(field), func(r R) any { return r.String() }) gotFast, gpF := callSafe(fast, func(R) any { return Get[string](fast, field) }) gotSlow, gpS := callSafe(slow, func(R) any { return Get[string](slow, field) }) if wp != gpF || wp != gpS { t.Fatalf("panic 行为不一致: reflux=%v 快路径=%v 回退=%v", wp, gpF, gpS) } if !wp && (want != gotFast || want != gotSlow) { t.Fatalf("string: reflux=%#v 快路径=%#v 回退=%#v", want, gotFast, gotSlow) } // int wantI, wpI := callSafe(slow.Get(field), func(r R) any { return r.Int() }) gotIF, gpIF := callSafe(fast, func(R) any { return Get[int](fast, field) }) gotIS, gpIS := callSafe(slow, func(R) any { return Get[int](slow, field) }) if wpI != gpIF || wpI != gpIS { t.Fatalf("int panic 行为不一致: reflux=%v 快=%v 回退=%v", wpI, gpIF, gpIS) } if !wpI && (wantI != gotIF || wantI != gotIS) { t.Fatalf("int: reflux=%#v 快=%#v 回退=%#v", wantI, gotIF, gotIS) } }) } } func TestGenericGetTypes(t *testing.T) { o := newUxOuter() r := New(o) if got := Get[string](r, "Mid", "Inner", "City"); got != "Beijing" { t.Fatalf("string = %q", got) } if got := Get[string](r, "Mid.Inner.City"); got != "Beijing" { t.Fatalf("点号路径 = %q", got) } if got := Get[int](r, "Mid.Inner.Zip"); got != 100000 { t.Fatalf("int = %d", got) } if got := Get[int64](r, "Mid.Inner.Zip"); got != 100000 { t.Fatalf("int64 = %d", got) } if got := Get[bool](r, "Mid.Inner.Active"); !got { t.Fatal("bool = false") } if got := Get[float64](r, "Mid.Inner.Ratio"); got != 1.5 { t.Fatalf("float64 = %v", got) } // 路径不存在 -> 零值 if got := Get[string](r, "NoSuch"); got != "" { t.Fatalf("不存在的路径应该返回零值, 得到 %q", got) } if got := Get[int](r, "NoSuch"); got != 0 { t.Fatalf("不存在的路径应该返回 0, 得到 %d", got) } // 未导出字段 -> 零值 if got := Get[string](r, "secret"); got != "" { t.Fatalf("未导出字段应该返回零值, 得到 %q", got) } } // 泛型方案的已知缺口: 具名标量类型不会命中 type switch 的 case, // 会掉进 default 分支走 Any() 断言 —— 行为与 Get(...).String() 不同。 // 这里把这个差异**显式钉住**, 免得以后当成 bug 排查。 func TestGenericNamedTypeGap(t *testing.T) { box := newConvBox() r := New(box) // NamedS 字段本身是 myStr 类型 // 1) 取成 string: 命中 case *string 快路径, 正常转换 if got := Get[string](r, "NamedS"); got != "named" { t.Fatalf("GetT[string] 取具名字段 = %q", got) } // 2) Str 字段是原生 string, 取成具名类型 myStr: // 走 default 分支的 Any().(myStr) 断言, 会失败并返回零值, // 而 Get("Str").String() 是能拿到 "hello" 的。 if got := Get[myStr](r, "Str"); got != "" { t.Logf("具名类型缺口已消失(实现改进了): GetT[myStr] = %q", got) } else { t.Log("已知缺口: GetT[具名类型] 在字段为原生类型时返回零值, 需在文档中说明") } } // 全局 sink: 防止编译器把"结果被丢弃"的调用优化掉, 导致测出假的 0 分配。 var ( sinkS string sinkI int sinkI64 int64 sinkB bool sinkF float64 sinkSS []string ) // 精确统计 Get[T] 各条分支的分配次数。 // testing.AllocsPerRun 比 -benchmem 更直接: 它给出的是每次调用的确切分配数。 func TestGenericAllocs(t *testing.T) { o := newUxOuter() box := newConvBox() fast := New(o) fastBox := New(box) slow := newLegacy(o) cases := []struct { name string want float64 // 期望的分配次数 f func() }{ // ---- 快路径: 应该全部 0 分配 ---- {"Get[string] 3层路径", 0, func() { sinkS = Get[string](fast, "Mid", "Inner", "City") }}, {"Get[string] 点号路径", 0, func() { sinkS = Get[string](fast, "Mid.Inner.City") }}, {"Get[string] 单层路径", 0, func() { sinkS = Get[string](fast, "Name") }}, {"Get[int]", 0, func() { sinkI = Get[int](fast, "Mid.Inner.Zip") }}, {"Get[int64]", 0, func() { sinkI64 = Get[int64](fast, "Mid.Inner.Zip") }}, {"Get[bool]", 0, func() { sinkB = Get[bool](fast, "Mid.Inner.Active") }}, {"Get[float64]", 0, func() { sinkF = Get[float64](fast, "Mid.Inner.Ratio") }}, {"Get[string] slice下标", 0, func() { sinkS = Get[string](fast, "Tags", "1") }}, {"Get[string] 路径不存在", 0, func() { sinkS = Get[string](fast, "NoSuch") }}, {"Get[string] 未导出字段", 0, func() { sinkS = Get[string](fast, "secret") }}, {"Get[int] 路径不存在", 0, func() { sinkI = Get[int](fast, "NoSuch") }}, // ---- 会分配的分支(预期之内, 但要说清楚) ---- {"Get[string] 跨类型(int=42, 命中strconv小整数缓存)", -1, func() { sinkS = Get[string](fastBox, "I") }}, {"Get[string] 跨类型(int=100000, 真实格式化)", -1, func() { sinkS = Get[string](fast, "Mid.Inner.Zip") }}, {"Get[string] 跨类型(float64)", -1, func() { sinkS = Get[string](fast, "Mid.Inner.Ratio") }}, {"Get[int] 跨类型(string字段)", -1, func() { sinkI = Get[int](fastBox, "NumStr") }}, {"Get[string] map键", -1, func() { sinkS = Get[string](fast, "Meta", "k1") }}, {"Get[[]string] default分支", -1, func() { sinkSS = Get[[]string](fast, "Tags") }}, {"Get[string] 回退到reflux", -1, func() { sinkS = Get[string](slow, "Mid.Inner.City") }}, // ---- 对照 ---- {"对照: Get(...).String()", -1, func() { sinkS = fast.Get("Mid", "Inner", "City").String() }}, {"对照: 内部 getString", -1, func() { sinkS = fast.(*rfx).getString("Mid", "Inner", "City") }}, {"对照: 参照实现 refx Get().String()", -1, func() { sinkS = slow.Get("Mid", "Inner", "City").String() }}, } t.Log("每次调用的分配次数:") for _, c := range cases { got := testing.AllocsPerRun(200, c.f) t.Logf(" %-32s %.0f allocs", c.name, got) if c.want >= 0 && got != c.want { t.Errorf("%s: 分配 %.0f 次, 期望 %.0f 次", c.name, got, c.want) } } } // --------------------------------------------------------------------------- // Get[T] 必须是 R 访问器集合的完整镜像 // --------------------------------------------------------------------------- type genAllDoc struct { Age int Ratio float64 Tags []any Names []string Nums []int Flags []bool Meta map[string]any SMeta map[string]string IMeta map[string]int I64Map map[string]int64 BMeta map[string]bool SSMeta map[string][]string } func newGenAllDoc() *genAllDoc { return &genAllDoc{ Age: 42, Ratio: 2.5, Tags: []any{"a", "b"}, Names: []string{"x", "y"}, Nums: []int{1, 2}, Flags: []bool{true, false}, Meta: map[string]any{"k": "v"}, SMeta: map[string]string{"k": "v"}, IMeta: map[string]int{"k": 1}, I64Map: map[string]int64{"k": 2}, BMeta: map[string]bool{"k": true}, SSMeta: map[string][]string{"k": {"a"}}, } } // Get[T] 与同名访问器必须给出相同结果 —— 尤其在"类型不完全匹配、需要转换"时。 // // 这是补齐类型分支的原因: 少一个分支就会掉进 default 的 Any().(T) 断言, // 断言不做转换, 于是 Get[int32] 读 int 字段会静默返回 0。 func TestGenericMirrorsAccessors(t *testing.T) { d := newGenAllDoc() r := New(d) cases := []struct { name string generic any accessor any }{ // 需要转换的标量(字段类型与 T 不一致) {"int8 <- int", Get[int8](r, "Age"), r.Get("Age").Int8()}, {"int16 <- int", Get[int16](r, "Age"), r.Get("Age").Int16()}, {"int32 <- int", Get[int32](r, "Age"), r.Get("Age").Int32()}, {"uint <- int", Get[uint](r, "Age"), r.Get("Age").Uint()}, {"uint8 <- int", Get[uint8](r, "Age"), r.Get("Age").Uint8()}, {"uint16 <- int", Get[uint16](r, "Age"), r.Get("Age").Uint16()}, {"uint32 <- int", Get[uint32](r, "Age"), r.Get("Age").Uint32()}, {"uint64 <- int", Get[uint64](r, "Age"), r.Get("Age").Uint64()}, {"float32 <- float64", Get[float32](r, "Ratio"), r.Get("Ratio").Float32()}, // 快路径的五种 {"string <- int", Get[string](r, "Age"), r.Get("Age").String()}, {"int <- int", Get[int](r, "Age"), r.Get("Age").Int()}, {"int64 <- int", Get[int64](r, "Age"), r.Get("Age").Int64()}, {"float64 <- float64", Get[float64](r, "Ratio"), r.Get("Ratio").Float64()}, // 切片: 需要逐元素转换 {"[]string <- []any", Get[[]string](r, "Tags"), r.Get("Tags").StringSlice()}, {"[]any <- []string", Get[[]any](r, "Names"), r.Get("Names").Slice()}, {"[]int <- []int", Get[[]int](r, "Nums"), r.Get("Nums").IntSlice()}, {"[]bool <- []bool", Get[[]bool](r, "Flags"), r.Get("Flags").BoolSlice()}, // map: 需要逐值转换 {"map[string]string <- map[string]any", Get[map[string]string](r, "Meta"), r.Get("Meta").StringMapString()}, {"map[string]any <- map[string]string", Get[map[string]any](r, "SMeta"), r.Get("SMeta").StringMap()}, {"map[string]int", Get[map[string]int](r, "IMeta"), r.Get("IMeta").StringMapInt()}, {"map[string]int64", Get[map[string]int64](r, "I64Map"), r.Get("I64Map").StringMapInt64()}, {"map[string]bool", Get[map[string]bool](r, "BMeta"), r.Get("BMeta").StringMapBool()}, {"map[string][]string", Get[map[string][]string](r, "SSMeta"), r.Get("SSMeta").StringMapStringSlice()}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { g := fmt.Sprintf("%v", c.generic) a := fmt.Sprintf("%v", c.accessor) if g != a { t.Fatalf("Get[T] 与访问器不一致\n Get[T] = %s\n 访问器 = %s", g, a) } // 顺带守住"没掉进 default 分支"这件事: 上面每一格都是需要转换的场景, // 掉进 Any().(T) 断言就会得到零值。 if g == "0" || g == "[]" || g == "map[]" { t.Fatalf("疑似掉进 default 分支返回了零值: %s", g) } }) } }