From 5c9f8bd6e7c3bc5ebc89b1abfd5115e96e52a5b7 Mon Sep 17 00:00:00 2001 From: what Date: Fri, 28 Aug 2026 16:37:04 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E7=94=A8=E7=B1=BB=E5=9E=8B=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E7=BC=93=E5=AD=98=20+=20=E6=8C=87=E9=92=88=E5=81=8F?= =?UTF-8?q?=E7=A7=BB=E5=AF=BB=E5=9D=80=E9=87=8D=E5=86=99=E7=83=AD=E8=B7=AF?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将实验目录 experiment/fastx 的实现合入主实现,替换原来逐次反射的做法。 实现方式: - 类型布局缓存: 首次遇到某类型时把字段偏移量构建成描述符存入全局缓存, 字段查找从按名线性比较变成 O(1) map 查表 (rfx_typedesc.go) - 指针偏移寻址: 取字段时用 基址+偏移量 直接算地址, 不再构造中间 reflect.Value (unsafeptr.go, 全部 unsafe 代码集中在这一个文件) - 零分配路径解析: 路径按需切片遍历, 不再为每次 Get 分配临时切片 (path.go) - 标量直读: String/Int/Bool/Float64 在类型匹配时直接按机器类型读内存, 绕开 interface 装箱和 cast 转换 语义复杂、调用频次低的操作(复合类型赋值、Append、Delete、容器转换)仍走原 reflect 实现, 保留为冷路径 refx (rfx_reflect.go), 避免重写引入行为偏差。 新增包级泛型函数 Get[T](r, path...), 按路径直接取出目标类型, 零内存分配。 语义与 r.Get(path...).Xxx() 严格等价, 不改动任何现有接口。 性能对比(同进程同数据, -benchmem -count=6 中位数): - Get 嵌套 + String 150.6ns/7allocs -> 50.7ns/2allocs 2.97x - Get 4 层深路径 234.1ns/10allocs -> 68.9ns/2allocs 3.40x - Set 单层 65.8ns/2allocs -> 18.1ns/0allocs 3.64x - Set 嵌套 122.1ns/3allocs -> 35.5ns/0allocs 3.44x - Exists 127.4ns/5allocs -> 37.2ns/1alloc 3.42x - 访问器 String 15.0ns/1alloc -> 1.3ns/0allocs 11.8x - Get[string] 泛型直取 -> 29.9ns/0allocs 5.04x 两处不快: map 键访问 1.25x(map 无稳定布局, 仍走 reflect); New 构造 0.67x(多一次描述符缓存查找, 一次性成本)。 内存安全: 未导出字段在读和写两处显式拦截 —— reflect.NewAt 构造的 Value 不带只读标记, 语言层面的导出规则保护在 unsafe 路径上失效, 必须自己拦。 不变式与评审要点见 unsafeptr.go 顶部注释。 测试: 新增 703 个用例全部通过, 含 go test -race。 其中接口契约逐方法对拍(含 panic 错误信息逐字比对)、标量转换全矩阵对拍、 []any 内嵌 R、循环引用等边角场景, 均以保留下来的 refx 作为参照实现做差分验证。 合并过程中据此发现并修复 11 处行为偏差。 --- README.md | 116 ++- path.go | 117 +++ reflux.go | 35 +- rfx.go | 1728 +++++++++++++++++++++--------------------- rfx_alias_test.go | 92 +++ rfx_bench_test.go | 294 +++++++ rfx_contract_test.go | 253 +++++++ rfx_conv_test.go | 158 ++++ rfx_edge_test.go | 299 ++++++++ rfx_generic.go | 73 ++ rfx_generic_test.go | 162 ++++ rfx_reflect.go | 1009 ++++++++++++++++++++++++ rfx_typedesc.go | 215 ++++++ rfx_unsafe_test.go | 516 +++++++++++++ unsafeptr.go | 125 +++ util.go | 19 +- 16 files changed, 4360 insertions(+), 851 deletions(-) create mode 100644 path.go create mode 100644 rfx_alias_test.go create mode 100644 rfx_bench_test.go create mode 100644 rfx_contract_test.go create mode 100644 rfx_conv_test.go create mode 100644 rfx_edge_test.go create mode 100644 rfx_generic.go create mode 100644 rfx_generic_test.go create mode 100644 rfx_reflect.go create mode 100644 rfx_typedesc.go create mode 100644 rfx_unsafe_test.go create mode 100644 unsafeptr.go diff --git a/README.md b/README.md index b021388..a83668b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Reflux 是一个 Go 语言包,提供了统一的接口用于访问和操作嵌 - 🎯 **类型安全**: 使用反射但保证类型安全 - 🔥 **增强类型转换**: 支持切片和结构体之间的智能转换(如 []any -> []T, map -> struct) - 🌀 **R 接口集成**: 支持直接传入 R 接口或 []R 切片,无缝集成反射值 -- 🚀 **高性能**: 优化的反射操作,低内存开销 +- ⚡ **泛型直取**: `reflux.Get[string](r, "Address", "City")` 直接返回目标类型,**零内存分配** +- 🚀 **高性能**: 类型布局缓存 + 指针偏移寻址,Get/Set 比逐次反射快 3~4 倍,访问器快 8~12 倍 - 📦 **零依赖**: 仅依赖 Go 标准库和 spf13/cast ## 安装 @@ -1063,6 +1064,50 @@ value := rfx.Get("Config.Database.Host").String() ## API 文档 +### 包级函数 Get[T] (泛型直取) + +```go +func Get[T any](r R, path ...string) T +``` + +按路径取值并直接返回目标类型,**不产生中间的 R 包装对象**。 + +```go +p := &Person{Name: "Alice", Address: Address{City: "Beijing"}} +r := reflux.New(p) + +city := reflux.Get[string](r, "Address", "City") // "Beijing" +city2 := reflux.Get[string](r, "Address.City") // 点号路径同样可用 +age := reflux.Get[int](r, "Age") +ok := reflux.Get[bool](r, "Active") +ratio := reflux.Get[float64](r, "Ratio") +``` + +语义与 `r.Get(path...).Xxx()` **严格等价**,可以放心替换: + +| 情况 | 行为 | +|---|---| +| 路径不存在 / 未导出字段 / 下标越界 | 返回 `T` 的零值 | +| 类型转换失败 | panic,错误信息与访问器方法完全一致 | +| 传入非本包实现的 `R` | 自动回退到 `r.Get(path...).Xxx()`,结果一致 | + +差别只在开销: 链式写法每次都要在堆上新建一个 `R` 包装对象, +而 `Get[T]` 直接把结果写进调用方的变量。 + +```go +r.Get("Address", "City").String() // 2 次分配 +reflux.Get[string](r, "Address", "City") // 0 次分配, 快约 1.7 倍 +``` + +**覆盖类型**: `string` / `int` / `int64` / `bool` / `float64` 走零分配快路径; +其余类型走通用路径,行为等同 `r.Get(path...).Any().(T)`。 + +**两点注意**: + +1. `T` 只出现在返回值里,Go 无法类型推导,必须显式写出 `Get[string](...)`。 +2. 快路径只认原生标量类型。`Get[MyStr](r, "Name")` 在字段是原生 `string` 时, + 会走通用路径并因类型断言失败返回零值 —— 这种场景请用 `r.Get("Name").String()`。 + ### Reflux 接口 ```go @@ -1117,6 +1162,75 @@ type Reflux interface { } ``` +## 性能 + +### 实现方式 + +热路径不再逐次走 `reflect` 的按名字段查找,而是: + +1. **类型布局缓存** —— 第一次遇到某个类型时,把它每个字段的**字节偏移量**、 + 元素大小等信息构建成描述符,存进全局缓存(`sync.Map`)。之后同类型直接命中, + 字段查找从"按名字线性比较"变成 O(1) 的 map 查表。 +2. **指针偏移寻址** —— 取字段时用 `基址 + 偏移量` 直接算出地址,不再构造中间的 + `reflect.Value`。 +3. **零分配路径解析** —— 路径字符串按需切片遍历,不再为每次 `Get` 分配临时切片。 +4. **标量直读** —— `String()`/`Int()`/`Bool()`/`Float64()` 等在类型匹配时直接按 + 机器类型读内存,绕开 `interface{}` 装箱和 `cast` 转换。 + +语义复杂、调用频次低的操作(复合类型赋值、`Append`、`Delete`、容器转换) +仍然走原来的 reflect 实现 —— 这些操作的语义琐碎,重写必然引入偏差, +而它们本来就不在性能热点上。 + +### 与旧版本(纯 reflect 实现)的对比 + +同一进程、同一数据结构、同一路径,`-benchmem -count=6` 取中位数 +(Apple M4 Pro / darwin-arm64 / go1.25.5): + +| 场景 | 旧版本 | 新版本 | 提速 | +|---|---|---|---:| +| `Get("Address","City").String()` | 150.6 ns / 152 B / 7 allocs | **50.7 ns / 56 B / 2 allocs** | **2.97x** | +| `Get("Address.City").String()` | 143.2 ns / 136 B / 6 allocs | **52.4 ns / 40 B / 2 allocs** | **2.73x** | +| `Get` 4 层深路径 | 234.1 ns / 280 B / 10 allocs | **68.9 ns / 88 B / 2 allocs** | **3.40x** | +| `Get("Tags","1")` slice 下标 | 134.9 ns / 152 B / 7 allocs | **47.9 ns / 56 B / 2 allocs** | **2.81x** | +| `Set("Name", ...)` | 65.8 ns / 32 B / 2 allocs | **18.1 ns / 0 B / 0 allocs** | **3.64x** | +| `Set("Address.City", ...)` | 122.1 ns / 80 B / 3 allocs | **35.5 ns / 0 B / 0 allocs** | **3.44x** | +| `Exists("Address","City")` | 127.4 ns / 112 B / 5 allocs | **37.2 ns / 32 B / 1 alloc** | **3.42x** | +| 访问器 `String()` (纯转换) | 15.0 ns / 16 B / 1 alloc | **1.3 ns / 0 B / 0 allocs** | **11.8x** | +| 访问器 `Int()` (纯转换) | 12.9 ns / 8 B / 1 alloc | **1.5 ns / 0 B / 0 allocs** | **8.5x** | + +泛型直取(新增 API,旧版本没有对应写法): + +| 场景 | 新版本 | 相对旧版链式 | +|---|---|---:| +| `Get[string](r, "Address", "City")` | **29.9 ns / 0 B / 0 allocs** | **5.04x** | +| `Get[string](r, "B","C","D","Leaf")` | **45.0 ns / 0 B / 0 allocs** | **5.20x** | + +参照基准线: 纯 Go 字段访问 `p.Address.City` 是 0.34 ns / 0 allocs。 + +### 两处不快的地方(如实说明) + +| 场景 | 旧版本 | 新版本 | 变化 | +|---|---|---|---:| +| `Get("Meta","k")` map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25x | +| `New(指针)` 构造 | 16.9 ns / 1 alloc | 25.4 ns / 1 alloc | **0.67x** | + +- **map 只快 1.25 倍**: map 没有稳定的内存布局可以做偏移量运算,这条路径完全走 + reflect,而且取出来的值必须拷一份(map 元素不可寻址)。这是设计上的取舍。 +- **`New` 慢了约 8 ns**: 构造时要查一次类型描述符缓存。这是一次性成本, + 换来之后每次 `Get`/`Set` 省下 50~100 ns —— 只要构造后至少访问一次就是净赚。 + +### 内存分配 + +分配次数的下降往往比 CPU 时间更有意义(GC 压力): + +- `Set`: **2 → 0** +- `Get` + 访问器: **7 → 2** +- `Exists`: **5 → 1** +- `Get[T]` 泛型直取: **0** + +`Get` 剩下的 2 次分配是 API 形状决定的下限: 一次是返回的 `R` 包装对象(24 字节), +一次是可变参数切片(通过接口调用时逃逸分析穿不透)。想完全避免就用 `Get[T]`。 + ## 注意事项 1. **指针 vs 值传递**: diff --git a/path.go b/path.go new file mode 100644 index 0000000..9f88979 --- /dev/null +++ b/path.go @@ -0,0 +1,117 @@ +package reflux + +import "strings" + +// pathIter 在不分配内存的前提下遍历路径片段。 +// +// reflux 现有的 expandPath 会对每个片段做 strings.Split 并 append 到一个新切片, +// Get("Address","City") 这样一次调用就要分配 4 次(2 次 Split 的结果切片 + +// append 扩容 2 次)。pathIter 只做字符串切片(sub-slice 不分配)。 +type pathIter struct { + parts []string + // one 用于"路径只有一个字符串"的场景(Set/Delete 的 key 参数)。 + // 走这条分支可以避免临时构造 []string{key} 带来的堆分配。 + one string + isOne bool + i int // 当前处理到第几个片段 + off int // 在当前片段内部的字节偏移 + n int // 已经产出的段数 + limit int // 最多产出多少段, -1 表示不限制 +} + +func newPathIter(parts []string) pathIter { + return pathIter{parts: parts, limit: -1} +} + +func (it *pathIter) numParts() int { + if it.isOne { + return 1 + } + return len(it.parts) +} + +func (it *pathIter) partAt(i int) string { + if it.isOne { + return it.one + } + return it.parts[i] +} + +// next 返回下一个非空路径段。空段(连续点号、前后缀点号)会被跳过, +// 与 reflux expandPath 忽略空字符串的行为一致。 +func (it *pathIter) next() (string, bool) { + if it.limit >= 0 && it.n >= it.limit { + return "", false + } + for it.i < it.numParts() { + s := it.partAt(it.i) + if it.off >= len(s) { + it.i++ + it.off = 0 + continue + } + rest := s[it.off:] + if j := strings.IndexByte(rest, '.'); j >= 0 { + it.off += j + 1 + if j == 0 { + continue // 空段, 跳过 + } + it.n++ + return rest[:j], true + } + it.i++ + it.off = 0 + it.n++ + return rest, true + } + return "", false +} + +func parseIndex(s string) (int, bool) { + if len(s) == 0 || len(s) > 18 { + return 0, false + } + neg := false + i := 0 + if s[0] == '-' { + neg = true + i = 1 + if len(s) == 1 { + return 0, false + } + } + n := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return 0, false + } + n = n*10 + int(c-'0') + } + if neg { + n = -n + } + return n, true +} + +// splitLastStr 是 splitLast 的单字符串版本, 避免为了调用 splitLast 而临时 +// 构造一个 []string(那会带来一次堆分配 —— Set 的热路径上不可接受)。 +func splitLastStr(key string) (parent pathIter, last string, ok bool) { + n := 0 + i := 0 + for i < len(key) { + j := i + for j < len(key) && key[j] != '.' { + j++ + } + if j > i { + last = key[i:j] + n++ + } + i = j + 1 + } + if n == 0 { + return pathIter{}, "", false + } + return pathIter{one: key, isOne: true, limit: n - 1}, last, true +} diff --git a/reflux.go b/reflux.go index 8e88601..9bab7a2 100644 --- a/reflux.go +++ b/reflux.go @@ -64,7 +64,12 @@ func New(v any) R { case R: return t case []R: - return &rfx{value: reflect.ValueOf(t)} + // []R 原样保留: 既不深拷贝也不包指针, 这样 Raw() 是 Slice kind, + // Array() 能取到里面的 R。 + rt := reflect.TypeOf(t) + box := reflect.New(rt) + box.Elem().Set(reflect.ValueOf(t)) + return &rfx{td: rfxDescriptorOf(rt), ptr: box.UnsafePointer(), writable: true} default: if t == valuex.Nil { return Nil @@ -112,9 +117,33 @@ func New(v any) R { // 如果原始传入的不是指针类型,需要进行深度克隆以避免修改原始数据 // 对于引用类型(map, slice)这尤其重要 if !isPtr { - // 使用深度克隆创建一个完全独立的副本 + // 使用深度克隆创建一个完全独立的副本(DeepClone 返回的是指针) rv = DeepClone(actualValue) } - return &rfx{value: rv} + return newRfx(rv) +} + +// newRfx 把一个 reflect.Value 转成 rfx 的内部表示(类型描述符 + 地址 + 可写标记)。 +func newRfx(rv reflect.Value) *rfx { + if rv.Kind() == reflect.Ptr { + if rv.IsNil() { + panic(ErrTargetNilPointer) + } + return &rfx{ + td: rfxDescriptorOf(rv.Type().Elem()), + ptr: rv.UnsafePointer(), + writable: true, + // 走到这里说明持有的是指针本身, Raw() 应当返回 reflect.Ptr + ptrRoot: true, + } + } + // 非指针值: 装箱一份以取得可寻址的地址 + box := reflect.New(rv.Type()) + box.Elem().Set(rv) + return &rfx{ + td: rfxDescriptorOf(rv.Type()), + ptr: box.UnsafePointer(), + writable: true, + } } diff --git a/rfx.go b/rfx.go index 47ddad5..96b9551 100644 --- a/rfx.go +++ b/rfx.go @@ -2,1001 +2,1043 @@ package reflux import ( "encoding/json" + "errors" "fmt" "reflect" "strconv" - "strings" + "unsafe" "git.fsdpf.net/go/reflux/valuex" "github.com/spf13/cast" ) -// rfx 是 R 接口的具体实现 -// 通过封装 reflect.Value 提供了对结构体、切片、映射等类型的统一访问方式 +// rfx 是 R 接口的实现。 +// +// 它的内部表示只有三个字段(24 字节), 而不是一个 reflect.Value: +// - td: 当前值的类型布局描述符(全局缓存, 不重复构建) +// - ptr: 指向一个 td.rtype 类型值的地址 +// - writable: 该地址是否允许写入 +// +// 见 unsafeptr.go 顶部的不变式 I1~I4。 type rfx struct { - // value 存储底层的反射值 - // 该值应该是指针类型,以支持修改操作 - value reflect.Value + td *rfxTypeDesc + ptr unsafe.Pointer + writable bool + // ptrRoot 标记"这个 rfx 是由 New(&x) 直接构造的根"。 + // reflux 的 rfx.value 在这种情况下存的是**指针**本身, 所以 Raw() 返回 + // reflect.Ptr; Get 出来的子值则存字段值。这个标记只为对齐 Raw() 的行为, + // 不参与路径遍历。(放在 writable 后面, 落在原有的对齐填充里, rfx 仍是 24 字节) + ptrRoot bool } -// Get 通过路径获取嵌套字段的值 -func (r *rfx) Get(p ...string) R { - return &rfx{value: getValueByPath(r.value, p...)} +// 编译期断言: 原型必须完整实现 R, 保证对外 API 签名不变。 +var _ R = (*rfx)(nil) + +// invalid 表示"路径不存在", 全局共享一份, 命中不存在的路径时不产生分配。 +var invalid = &rfx{} + +func (v *rfx) valid() bool { return v != nil && v.td != nil && v.ptr != nil } + +// --------------------------------------------------------------------------- +// 路径遍历 +// +// 遍历过程中的中间状态全部用**值传递**的 rfx(24 字节, 完全落在栈上), +// 只有最终要返回给调用方的那一个才装箱到堆上。这样一次 Get 无论路径多长 +// 都只有 1 次分配 —— 而这 1 次是 R 链式 API 形状决定的下限。 +// --------------------------------------------------------------------------- + +// normalize 把当前值解引用到"实际类型": 穿过指针链, 拆开 interface。 +// 与 reflux 的 derefWithAccessor 对应, 同样支持解开 valuex.Accessor。 +func (v rfx) normalize() (rfx, bool) { + for { + if v.td == nil || v.ptr == nil { + return rfx{}, false + } + switch v.td.Kind { + case reflect.Ptr: + p := loadPtr(v.ptr) + if p == nil { + return rfx{}, false + } + v.ptr = p + v.td = v.td.Elem + case reflect.Interface: + // interface 没有固定布局可以做偏移量运算, 退回 reflect 拆包 + rv := valueAt(v.td, v.ptr) + if rv.IsNil() { + return rfx{}, false + } + // 支持 interface 里装着 valuex.Accessor / R 的情况 + if acc, ok := rv.Interface().(valuex.Accessor); ok { + raw := acc.Raw() + if !raw.IsValid() { + return rfx{}, false + } + return fromReflect(raw, v.writable) + } + return fromReflect(rv.Elem(), v.writable) + default: + return v, true + } + } } -// Scope 创建一个指定路径的作用域视图(深度克隆) -func (r *rfx) Scope(p ...string) R { - v := getValueByPath(r.value, p...) - if !v.IsValid() { - return &rfx{value: reflect.Value{}} +// fromReflect 把一个 reflect.Value 转换成 rfx 表示。 +// 不可寻址的值会被拷贝到堆上(boxCopy), 并标记为不可写 —— 与 reflect 里 +// "map 元素、interface 拆包结果不可寻址" 的语义一致。 +func fromReflect(rv reflect.Value, writable bool) (rfx, bool) { + if !rv.IsValid() { + return rfx{}, false + } + switch { + case rv.Kind() == reflect.Ptr: + if rv.IsNil() { + return rfx{}, false + } + return rfx{ + td: rfxDescriptorOf(rv.Type().Elem()), + ptr: rv.UnsafePointer(), + writable: writable, + }.normalize() + case rv.CanAddr(): + return rfx{ + td: rfxDescriptorOf(rv.Type()), + ptr: addrOf(rv), + writable: writable && rv.CanSet(), + }.normalize() + default: + return rfx{ + td: rfxDescriptorOf(rv.Type()), + ptr: boxCopy(rv), + writable: false, + }.normalize() } - - // 深度克隆值 - cloned := DeepClone(v) - return &rfx{value: cloned} } -// Set 设置指定路径的值,支持链式调用 -// 如果路径不存在或设置失败,会 panic 并提供详细错误信息 -func (r *rfx) Set(key string, v any) R { - // 展开路径 - keys := expandPath(key) - if len(keys) == 0 { - panic(ErrEmptyPath) +// step 沿路径前进一段。 +func (v rfx) step(seg string) (rfx, bool) { + cur, ok := v.normalize() + if !ok { + return rfx{}, false } - // 如果只有一个键,直接设置 - if len(keys) == 1 { - target := r.getParentValue() - if !target.IsValid() { - panic(NewErrInvalidValueForPath(key)) - } - if !target.CanSet() { - panic(NewErrCannotSetPath(key)) - } - if err := r.setFieldValue(target, keys[0], v); err != nil { - panic(NewErrSetFailed(key, err)) - } - return r - } - - // 多个键的情况,需要特殊处理 map 中的 struct - if err := r.setNestedValue(r.value, keys, v); err != nil { - panic(NewErrSetFailed(key, err)) - } - return r -} - -// Append 追加指定路径的值 -// 参数 items 为要追加的值 -// 返回当前 R 实例以支持链式调用 -func (r *rfx) Append(items ...any) R { - // 没有要追加的元素,直接返回 - if len(items) == 0 { - return r - } - - target := r.getParentValue() - target = deref(target) - - if !target.IsValid() { - panic(ErrAppendNilValue) - } - if target.Kind() != reflect.Slice { - panic(ErrAppendNotSupported) - } - - // 一次性构造所有要追加的元素,然后调用一次 reflect.Append - elemType := target.Type().Elem() - newValues := make([]reflect.Value, len(items)) - for i, item := range items { - newElem := reflect.New(elemType).Elem() - if err := r.setValue(newElem, item); err != nil { - panic(NewErrAppendItemFailed(i, err)) - } - newValues[i] = newElem - } - - target.Set(reflect.Append(target, newValues...)) - return r -} - -// setNestedValue 递归设置嵌套值,特殊处理 map 中的 struct -func (r *rfx) setNestedValue(current reflect.Value, keys []string, v any) error { - // 解引用指针和接口 - current = derefWithAccessor(current) - if !current.IsValid() { - return ErrNilPointerInPath - } - - // 如果只剩一个键,直接设置 - if len(keys) == 1 { - if !current.CanSet() { - return ErrValueCannotBeSet - } - return r.setFieldValue(current, keys[0], v) - } - - // 多个键的情况 - firstKey := keys[0] - remainingKeys := keys[1:] - - switch current.Kind() { + switch cur.td.Kind { case reflect.Struct: - field := tryStructFieldValue(current, firstKey) - if !field.IsValid() { - return NewErrFieldNotFound(firstKey) + fd := cur.td.lookupField(seg) + if fd == nil { + return rfx{}, false } - return r.setNestedValue(field, remainingKeys, v) + // I3: 未导出字段一律拒绝。reflect.NewAt 出来的 rfx 不带只读标记, + // 放行会让调用方绕过 Go 的导出规则直接读写私有字段。 + if !fd.Exported { + return rfx{}, false + } + return rfx{ + td: fd.Type, + ptr: fieldAt(cur.ptr, fd.Offset), + writable: cur.writable, + }, true + + case reflect.Slice: + idx, ok := parseIndex(seg) + if !ok { + return rfx{}, false + } + p := sliceElemAt(cur.ptr, idx, cur.td.elemSize) + if p == nil { + return rfx{}, false + } + // 切片元素落在底层数组里, 本身总是可寻址的 —— 与 reflect 的 + // rfx.Index(i).CanSet() == true 保持一致。 + return rfx{td: cur.td.Elem, ptr: p, writable: true}, true + + case reflect.Array: + idx, ok := parseIndex(seg) + if !ok { + return rfx{}, false + } + p := arrayElemAt(cur.ptr, idx, cur.td.arrayLen, cur.td.elemSize) + if p == nil { + return rfx{}, false + } + return rfx{td: cur.td.Elem, ptr: p, writable: cur.writable}, true case reflect.Map: - // Map 的特殊处理 - // 使用 tryMapFieldKey 获取实际的键 - actualKey := tryMapFieldKey(current, firstKey) - if !actualKey.IsValid() { - return NewErrKeyNotFound(firstKey) + // map 没有稳定布局, 退回 reflect + m := valueAt(cur.td, cur.ptr) + if m.IsNil() { + return rfx{}, false } - - mapValue := current.MapIndex(actualKey) - if !mapValue.IsValid() { - return NewErrValueNotFound(firstKey) + mv := tryMapFieldValue(m, seg) + if !mv.IsValid() { + return rfx{}, false } + return fromReflect(mv, false) - // 解开 interface 包装获取实际的值 - actualValue := mapValue - for actualValue.Kind() == reflect.Interface && !actualValue.IsNil() { - actualValue = actualValue.Elem() - } - - // 创建 map 值的副本以便修改 - valueCopy := reflect.New(actualValue.Type()).Elem() - valueCopy.Set(actualValue) - - // 在副本上递归设置值 - if err := r.setNestedValue(valueCopy, remainingKeys, v); err != nil { - return err - } - - // 将修改后的值设置回 map,使用实际找到的键 - current.SetMapIndex(actualKey, valueCopy) - return nil - - case reflect.Slice, reflect.Array: - idx, err := strconv.Atoi(firstKey) - if err != nil || idx < 0 || idx >= current.Len() { - return NewErrInvalidIndex(firstKey, current.Len()) - } - elem := current.Index(idx) - if !elem.IsValid() { - return NewErrElementInvalid(firstKey) - } - return r.setNestedValue(elem, remainingKeys, v) + default: + return rfx{}, false } - - return NewErrUnsupportedType(current.Kind(), firstKey) } -// getParentValue 获取父级值的辅助方法 -func (r *rfx) getParentValue(p ...string) reflect.Value { - if len(p) == 0 { - v := r.value - for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { - if v.IsNil() { - return reflect.Value{} - } - v = v.Elem() +// walk 沿整条路径前进, 全程不分配。 +func (v rfx) walk(it pathIter) (rfx, bool) { + cur := v + for { + seg, more := it.next() + if !more { + break } + var ok bool + cur, ok = cur.step(seg) + if !ok { + return rfx{}, false + } + } + return cur.normalize() +} + +// boxed 把栈上的遍历结果装箱成返回给调用方的 *rfx(唯一一次分配)。 +func boxed(v rfx, ok bool) R { + if !ok { + return invalid + } + out := v + return &out +} + +// --------------------------------------------------------------------------- +// R 接口: 热路径 +// --------------------------------------------------------------------------- + +// Get 通过路径获取嵌套字段, struct 字段查找走 map。 +func (v *rfx) Get(p ...string) R { + if !v.valid() { + return invalid + } + return boxed(v.walk(newPathIter(p))) +} + +// 下面五个 xxxValue 是标量直取的**值接收者**辅助方法。 +// +// 它们存在的唯一理由是挡住逃逸: 如果直接在 getXxx 里写 (&cur).String(), +// 局部变量 cur 就会逃逸到堆 —— Go 的逃逸分析是流不敏感的, 只要有一条路径 +// 把 &cur 交出去, cur 在所有路径上都得堆分配, 快路径那 0 分配立刻退化成 1 次。 +// 换成值接收者之后, 逃逸的是本方法的参数副本, 与调用方的 cur 无关。 +func (v rfx) stringValue() string { + if v.td != nil && v.ptr != nil && v.td.Kind == reflect.String { + return loadString(v.ptr) + } + return (&v).String() +} + +func (v rfx) intValue() int { + if n, ok := v.scalarInt64(); ok { + return int(n) + } + return (&v).Int() +} + +func (v rfx) int64Value() int64 { + if n, ok := v.scalarInt64(); ok { + return n + } + return (&v).Int64() +} + +func (v rfx) boolValue() bool { + if v.td != nil && v.ptr != nil && v.td.Kind == reflect.Bool { + return loadBool(v.ptr) + } + return (&v).Bool() +} + +func (v rfx) float64Value() float64 { + if f, ok := v.scalarFloat64(); ok { + return f + } + return (&v).Float64() +} + +// getString 等价于 Get(p...).String(), 但不产生中间的 R 包装对象。 +func (v *rfx) getString(p ...string) string { + if !v.valid() { + return "" + } + cur, ok := v.walk(newPathIter(p)) + if !ok { + return "" + } + return cur.stringValue() +} + +// getInt 等价于 Get(p...).Int(), 但不分配。 +func (v *rfx) getInt(p ...string) int { + if !v.valid() { + return 0 + } + cur, ok := v.walk(newPathIter(p)) + if !ok { + return 0 + } + return cur.intValue() +} + +// getInt64 等价于 Get(p...).Int64(), 但不分配。 +func (v *rfx) getInt64(p ...string) int64 { + if !v.valid() { + return 0 + } + cur, ok := v.walk(newPathIter(p)) + if !ok { + return 0 + } + return cur.int64Value() +} + +// getBool 等价于 Get(p...).Bool(), 但不分配。 +func (v *rfx) getBool(p ...string) bool { + if !v.valid() { + return false + } + cur, ok := v.walk(newPathIter(p)) + if !ok { + return false + } + return cur.boolValue() +} + +// getFloat64 等价于 Get(p...).Float64(), 但不分配。 +func (v *rfx) getFloat64(p ...string) float64 { + if !v.valid() { + return 0 + } + cur, ok := v.walk(newPathIter(p)) + if !ok { + return 0 + } + return cur.float64Value() +} + +func (v *rfx) Set(key string, val any) R { + if !v.valid() { + panic(NewErrInvalidValueForPath(key)) + } + parentIt, last, ok := splitLastStr(key) + if !ok { + panic(ErrEmptyPath) + } + parent, ok := v.walk(parentIt) + if !ok || !parent.writable { + // 两种情况交回 reflux 现有实现处理: + // 1. 路径不存在 —— 让它给出与现状一致的错误信息 + // 2. 父级不可写 —— 典型是路径穿过了 map: map 元素不可寻址, 取到的是 + // 副本, 直接写副本不会反映到原 map 上。reflux 有完整的 + // "取出-修改-写回 SetMapIndex" 逻辑, 复用它而不是重新实现一遍。 + v.slow().Set(key, val) return v } - return getValueByPath(r.value, p...) + if err := parent.setField(last, val); err != nil { + panic(NewErrSetFailed(key, err)) + } + return v } -// setFieldValue 设置字段值的辅助方法 -// 返回 error 包含详细的设置失败信息 -func (r *rfx) setFieldValue(target reflect.Value, key string, v any) error { - target = deref(target) - if !target.IsValid() { - return ErrTargetNilPointer - } - - switch target.Kind() { +// setField 在 parent 上设置名为 seg 的字段/键/下标。 +func (v rfx) setField(seg string, val any) error { + switch v.td.Kind { case reflect.Struct: - field := tryStructFieldValue(target, key) - if !field.IsValid() { - return NewErrFieldNotFoundInStruct(key, target.Type()) + fd := v.td.lookupField(seg) + if fd == nil { + return NewErrFieldNotFoundInStruct(seg, v.td.rtype) } - if !field.CanSet() { - return NewErrFieldCannotSet(key, field.Type()) + // I3 + I4: 未导出字段、或整条访问链本身不可写, 都必须拒绝。 + // reflect 路径上这两件事是 CanSet() 免费帮我们做的, unsafe 路径必须手写。 + if !fd.Exported { + return NewErrFieldCannotSet(seg, fd.Type.rtype) } - return r.setValue(field, v) - case reflect.Map: - if target.IsNil() { - target.Set(reflect.MakeMap(target.Type())) + if !v.writable { + return NewErrFieldCannotSet(seg, fd.Type.rtype) } + return v.assignField(fd.Type, fieldAt(v.ptr, fd.Offset), seg, val) - // 先尝试使用 tryMapFieldKey 检查字段是否已存在并获取实际的键 - actualKey := tryMapFieldKey(target, key) - if actualKey.IsValid() { - // 字段已存在,创建新值用于设置 - newValue := reflect.New(target.Type().Elem()).Elem() - if err := r.setValue(newValue, v); err != nil { - return err - } - target.SetMapIndex(actualKey, newValue) - return nil - } - - // 字段不存在,创建新的 map 值 - // 处理 nil 值的情况 - if v == nil { - target.SetMapIndex(reflect.ValueOf(key), reflect.Zero(target.Type().Elem())) - return nil - } - - val := reflect.ValueOf(v) - if !val.Type().AssignableTo(target.Type().Elem()) { - // 尝试转换 - if val.Type().ConvertibleTo(target.Type().Elem()) { - val = val.Convert(target.Type().Elem()) - } else { - return NewErrCannotAssign(val.Type(), target.Type().Elem()) - } - } - target.SetMapIndex(reflect.ValueOf(key), val) - return nil case reflect.Slice: - idx, err := strconv.Atoi(key) - if err != nil { - return NewErrInvalidSliceIndex(key, err) + idx, ok := parseIndex(seg) + if !ok { + return NewErrInvalidSliceIndex(seg, fmt.Errorf("不是合法下标")) } - - // 对于切片,支持使用索引 -1 追加新元素(插入到切片前面) if idx == -1 { - elemType := target.Type().Elem() - newElem := reflect.New(elemType).Elem() - if err := r.setValue(newElem, v); err != nil { - return err - } - // 将新元素放在前面,原有元素顺序后移 - newSlice := reflect.MakeSlice(target.Type(), 0, target.Len()+1) - newSlice = reflect.Append(newSlice, newElem) - newSlice = reflect.AppendSlice(newSlice, target) - target.Set(newSlice) - return nil + // reflux 用 -1 表示前插, 要重新分配底层数组, 交给它处理 + return v.slowSetField(seg, val) } + p := sliceElemAt(v.ptr, idx, v.td.elemSize) + if p == nil { + // 与 reflux 一致: 越界是错误, 不会自动扩容。 + // 这里直接构造错误而不是委托, 否则错误信息会被包装两层。 + return NewErrSliceIndexOutOfRange(idx, sliceLen(v.ptr)) + } + return v.assignField(v.td.Elem, p, seg, val) - if idx < 0 || idx >= target.Len() { - return NewErrSliceIndexOutOfRange(idx, target.Len()) - } - elem := target.Index(idx) - if !elem.CanSet() { - return NewErrSliceElementCannotSet(idx) - } - return r.setValue(elem, v) case reflect.Array: - idx, err := strconv.Atoi(key) - if err != nil { - return NewErrInvalidArrayIndex(key, err) + idx, ok := parseIndex(seg) + if !ok { + return NewErrInvalidArrayIndex(seg, fmt.Errorf("不是合法下标")) } - if idx < 0 || idx >= target.Len() { - return NewErrArrayIndexOutOfRange(idx, target.Len()) + p := arrayElemAt(v.ptr, idx, v.td.arrayLen, v.td.elemSize) + if p == nil { + return NewErrArrayIndexOutOfRange(idx, v.td.arrayLen) } - elem := target.Index(idx) - if !elem.CanSet() { + if !v.writable { return NewErrArrayElementCannotSet(idx) } - return r.setValue(elem, v) + return v.assignField(v.td.Elem, p, seg, val) + + case reflect.Map: + return v.slowSetField(seg, val) + + default: + return NewErrUnsupportedTargetType(v.td.Kind) } - return NewErrUnsupportedTargetType(target.Kind()) } -// setValue 设置值的辅助方法 -// 使用 cast 库进行智能类型转换,支持更多的转换场景 -// 返回 error 包含详细的类型转换失败信息 -func (r *rfx) setValue(field reflect.Value, v any) error { - val, _, err := normalizeInputValue(v) - - if err != nil { - return NewErrNormalizeInputFailed(err) +// slowSetField 把设置操作交回 reflux 现有实现, 保证语义完全一致。 +func (v rfx) slowSetField(seg string, val any) (err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = stripSetFailed(e) + } else { + err = fmt.Errorf("%v", r) + } + } + }() + if !v.writable { + return NewErrCannotSetPath(seg) } + v.slow().Set(seg, val) + return nil +} - targetType := field.Type() - - if !val.IsValid() { - val = reflect.Zero(targetType) +// stripSetFailed 剥掉 reflux 自己加的一层 ErrSetFailed 包装。 +// +// reflux 的 Set 失败时 panic 的是 NewErrSetFailed(seg, inner), 我们 recover 之后 +// 外层还会再包一次完整路径, 不剥就会得到 +// "set failed at path 'Items.10': set failed at path '10': index out of range" +// 这种双层信息, 与现状不一致。 +func stripSetFailed(err error) error { + type multi interface{ Unwrap() []error } + if m, ok := err.(multi); ok { + parts := m.Unwrap() + if len(parts) == 2 && errors.Is(parts[0], ErrSetFailed) { + return parts[1] + } } + return err +} - // 如果 val 有效,进行正常的类型处理 - // 统一解开最外层的 interface 包装,便于后续根据底层实际类型做处理 - for val.Kind() == reflect.Interface && !val.IsNil() { - val = val.Elem() - } - - // 尝试直接赋值(类型完全匹配) - if val.Type().AssignableTo(targetType) { - field.Set(val) +// assignField 是所有字段赋值的统一入口, 按"能多快就多快, 但语义必须与 reflux 一致" +// 的原则分三级: +// +// 1. 类型完全一致的常见标量 -> 直接按 *T 写内存, 零分配 +// 2. 其它标量目标 -> 用 cast 转换(由 conv_equiv_test 全矩阵对拍保证一致) +// 3. 复合目标(指针/切片/结构体/map/interface) -> **交回 reflux** +// +// 第 3 级不能自己实现: reflux 的 setValue 对这些类型有一整套语义 —— +// 指针字段是"设置指针指向的值"而不是替换指针、[]any 会逐元素转成目标切片类型、 +// map 可以填充进 struct。重新实现一遍必然出现行为偏差 +// (ExampleNew_withPointer 就是这么发现的)。 +func (v rfx) assignField(td *rfxTypeDesc, p unsafe.Pointer, seg string, val any) error { + if storeFast(td, p, val) { return nil } - - // 如果源值是指针但目标不是指针,尝试解引用后再赋值 - if val.Kind() == reflect.Ptr && !val.IsNil() && targetType.Kind() != reflect.Ptr { - derefVal := val.Elem() - if derefVal.Type().AssignableTo(targetType) { - field.Set(derefVal) + // 类型完全匹配: 直接赋值, 不必绕道 + if val != nil { + if rv := reflect.ValueOf(val); rv.IsValid() && rv.Type() == td.rtype { + valueAt(td, p).Set(rv) return nil } - // 解引用后继续使用下面的逻辑处理 - val = derefVal } + if td.isScalar() { + return assignReflect(valueAt(td, p), val) + } + return v.slowSetField(seg, val) +} - switch targetType.Kind() { - case reflect.Ptr: - // 处理指针类型 - // 如果传入的值已经是指针类型,尝试直接赋值 - if val.Type().AssignableTo(targetType) { - field.Set(val) +// storeFast 在"字段类型与传入值类型完全一致"时直接按类型写内存, 零分配、零转换。 +// 返回 false 表示走不了快路径, 需要回退。 +func storeFast(td *rfxTypeDesc, p unsafe.Pointer, val any) bool { + switch td.fast { + case fkString: + if s, ok := val.(string); ok { + storeString(p, s) + return true + } + case fkInt: + if i, ok := val.(int); ok { + storeInt(p, i) + return true + } + case fkBool: + if b, ok := val.(bool); ok { + storeBool(p, b) + return true + } + case fkFloat64: + if f, ok := val.(float64); ok { + storeFloat64(p, f) + return true + } + } + return false +} + +// assignReflect 是通用赋值回退路径, 与 reflux setValue 的转换策略一致: +// 可直接赋值 -> 赋值; 可转换 -> 转换后赋值; 否则用 cast 做智能转换。 +func assignReflect(dst reflect.Value, val any) error { + if val == nil { + dst.Set(reflect.Zero(dst.Type())) + return nil + } + // 解开 valuex.Accessor / R + if acc, ok := val.(valuex.Accessor); ok { + if raw := acc.Raw(); raw.IsValid() { + val = raw.Interface() + } else { + dst.Set(reflect.Zero(dst.Type())) return nil } - - // 如果传入的值不是指针,创建新指针并设置值 - elemType := targetType.Elem() - newPtr := reflect.New(elemType) - - // 递归设置指针指向的值 - if err := r.setValue(newPtr.Elem(), v); err != nil { - return err - } - - field.Set(newPtr) - return nil - - case reflect.Slice: // 处理切片类型,支持从通用切片(如 []any)转换 - if val.Kind() != reflect.Slice && val.Kind() != reflect.Array { - return NewErrCannotConvertToSlice(val.Type(), targetType) - } - - newSlice := reflect.MakeSlice(targetType, val.Len(), val.Len()) - - for i := 0; i < val.Len(); i++ { - if err := r.setValue(newSlice.Index(i), val.Index(i).Interface()); err != nil { - return NewErrSetSliceElementFailed(i, err) - } - } - - field.Set(newSlice) - - return nil - - case reflect.Struct: - // 遍历目标结构体的字段,从源值(结构体或 map)中按字段名取值并设置 - // 仅支持从 struct 或 map 填充 - if val.Kind() != reflect.Struct && val.Kind() != reflect.Map { - return NewErrCannotConvertToStruct(val.Type(), targetType) - } - - fieldType := field.Type() - for i := 0; i < fieldType.NumField(); i++ { - dstField := field.Field(i) - if !dstField.CanSet() { - continue - } - - valField := getValueByPath(val, fieldType.Field(i).Name) - - if !valField.IsValid() { - continue - } - - // 使用 setValue 复用现有的类型转换逻辑,忽略单个字段失败 - if err := r.setValue(dstField, valField.Interface()); err != nil { - return NewErrSetStructFieldFailed(fieldType.Field(i).Name, err) - } - } - return nil - } - - // 优先使用 cast 进行智能类型转换 - // 这样可以处理 string <-> number, number <-> bool 等常见转换 - var converted any - - switch targetType.Kind() { - case reflect.Bool: - converted, err = cast.ToBoolE(v) - case reflect.Int: - converted, err = cast.ToIntE(v) - case reflect.Int8: - converted, err = cast.ToInt8E(v) - case reflect.Int16: - converted, err = cast.ToInt16E(v) - case reflect.Int32: - converted, err = cast.ToInt32E(v) - case reflect.Int64: - converted, err = cast.ToInt64E(v) - case reflect.Uint: - converted, err = cast.ToUintE(v) - case reflect.Uint8: - converted, err = cast.ToUint8E(v) - case reflect.Uint16: - converted, err = cast.ToUint16E(v) - case reflect.Uint32: - converted, err = cast.ToUint32E(v) - case reflect.Uint64: - converted, err = cast.ToUint64E(v) - case reflect.Float32: - converted, err = cast.ToFloat32E(v) - case reflect.Float64: - converted, err = cast.ToFloat64E(v) + rv := reflect.ValueOf(val) + dt := dst.Type() + if rv.Type().AssignableTo(dt) { + dst.Set(rv) + return nil + } + switch dt.Kind() { case reflect.String: - converted, err = cast.ToStringE(v) - default: - return NewErrUnsupportedTargetTypeForValue(targetType, fmt.Sprintf("%T", v)) - } - - if err != nil { - // 如果 cast 失败,尝试标准的反射类型转换作为后备 - if val.IsValid() && val.Type().ConvertibleTo(field.Type()) { - field.Set(val.Convert(field.Type())) - return nil - } - return NewErrCannotConvertValue(fmt.Sprintf("%T", v), targetType, err) - } - - field.Set(reflect.ValueOf(converted)) - - return nil -} - -// Delete 删除指定路径的值,支持链式调用 -// 如果删除失败会 panic -func (r *rfx) Delete(p ...string) R { - if len(p) == 0 { - panic(ErrEmptyPath) - } - - // 展开路径 - keys := expandPath(p...) - if len(keys) == 0 { - panic("rfx: empty path") - } - - target := r.getParentValue(keys[:len(keys)-1]...) - if !target.IsValid() { - panic(fmt.Sprintf("rfx: invalid path '%s'", strings.Join(keys[:len(keys)-1], "."))) - } - - lastKey := keys[len(keys)-1] - for target.Kind() == reflect.Ptr || target.Kind() == reflect.Interface { - if target.IsNil() { - panic(fmt.Sprintf("rfx: nil value at path '%s'", strings.Join(keys[:len(keys)-1], "."))) - } - target = target.Elem() - } - - switch target.Kind() { - case reflect.Map: - target.SetMapIndex(reflect.ValueOf(lastKey), reflect.Value{}) - return r - case reflect.Slice: - idx, err := strconv.Atoi(lastKey) + s, err := cast.ToStringE(val) if err != nil { - panic(fmt.Sprintf("rfx: invalid slice index '%s'", lastKey)) + return NewErrCannotAssign(rv.Type(), dt) } - if idx < 0 || idx >= target.Len() { - panic(fmt.Sprintf("rfx: slice index out of range: %d", idx)) + dst.SetString(s) + return nil + case reflect.Bool: + b, err := cast.ToBoolE(val) + if err != nil { + return NewErrCannotAssign(rv.Type(), dt) } - // 删除切片元素 - newSlice := reflect.AppendSlice( - target.Slice(0, idx), - target.Slice(idx+1, target.Len()), - ) - target.Set(newSlice) - return r - default: - panic(fmt.Sprintf("rfx: cannot delete from type %s", target.Kind())) - } -} - -// Exists 检查指定路径的值是否存在 -func (r *rfx) Exists(p ...string) bool { - return getValueByPath(r.value, p...).IsValid() -} - -// Array 将当前值转换为 R 切片 -func (r *rfx) Array() []R { - v := r.value - for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { - if v.IsNil() { - return nil + dst.SetBool(b) + return nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := cast.ToInt64E(val) + if err != nil { + return NewErrCannotAssign(rv.Type(), dt) } - v = v.Elem() - } - - if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + if dst.OverflowInt(i) { + return NewErrCannotAssign(rv.Type(), dt) + } + dst.SetInt(i) + return nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + u, err := cast.ToUint64E(val) + if err != nil { + return NewErrCannotAssign(rv.Type(), dt) + } + if dst.OverflowUint(u) { + return NewErrCannotAssign(rv.Type(), dt) + } + dst.SetUint(u) + return nil + case reflect.Float32, reflect.Float64: + f, err := cast.ToFloat64E(val) + if err != nil { + return NewErrCannotAssign(rv.Type(), dt) + } + dst.SetFloat(f) return nil } - - result := make([]R, v.Len()) - for i := 0; i < v.Len(); i++ { - elem := v.Index(i) - if elem.Kind() == reflect.Interface && !elem.IsNil() { - if r, ok := elem.Interface().(R); ok { - result[i] = r - continue - } - } - result[i] = &rfx{value: elem} + if rv.Type().ConvertibleTo(dt) { + dst.Set(rv.Convert(dt)) + return nil } - return result + return NewErrCannotAssign(rv.Type(), dt) } -// keys 返回当前映射或结构体的所有键名 -func (r *rfx) Keys() []string { - v := r.value - for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { - if v.IsNil() { - return nil - } - v = v.Elem() +// Exists 检查路径是否存在。 +func (v *rfx) Exists(p ...string) bool { + if !v.valid() { + return false } + if len(p) == 0 { + // 必须规范化: 值可能是空指针, 或是装着 Nil 的 interface, + // 这些情况下 Exists() 应该为 false。 + _, ok := (*v).normalize() + return ok + } + _, ok := v.walk(newPathIter(p)) + return ok +} - switch v.Kind() { - case reflect.Map: - keys := v.MapKeys() - result := make([]string, len(keys)) - for i, k := range keys { - result[i] = fmt.Sprint(k.Interface()) +// --------------------------------------------------------------------------- +// R 接口: 冷路径 —— 直接委托给 reflux 现有实现, 保证行为完全一致 +// --------------------------------------------------------------------------- + +// slow 把当前值交给基于 reflect 的冷路径实现 refx。 +// 传的是 *T 指针, 所以不会发生深拷贝, 写操作落在同一块内存上。 +func (v rfx) slow() R { + if v.td == nil || v.ptr == nil { + return Nil + } + // 直接构造 refx: 手上已经是一个合法的 *T 指针, 不需要再走一遍 + // New 的类型校验和可能的深拷贝。 + return &refx{value: ptrValueAt(v.td, v.ptr)} +} + +func (v *rfx) Scope(p ...string) R { + target := v.Get(p...) + tv, ok := target.(*rfx) + if !ok || !tv.valid() { + return Nil + } + return tv.slow().Scope() +} + +func (v *rfx) Append(items ...any) R { + v.slow().Append(items...) + return v +} + +func (v *rfx) Delete(p ...string) R { + v.slow().Delete(p...) + return v +} + +func (v *rfx) Array() []R { + if !v.valid() { + return nil + } + switch v.td.Kind { + case reflect.Slice: + n := sliceLen(v.ptr) + // 批量分配: 一次开出 n 个 rfx 的连续内存, 再取每个元素的地址, + // 把 n+1 次堆分配压成 2 次。元素游标本来就同生共死, 连续存放也更缓存友好。 + buf := make([]rfx, n) + out := make([]R, n) + for i := 0; i < n; i++ { + buf[i] = rfx{ + td: v.td.Elem, + ptr: sliceElemAt(v.ptr, i, v.td.elemSize), + writable: true, + } + out[i] = &buf[i] } - return result - case reflect.Struct: - t := v.Type() - result := make([]string, t.NumField()) - for i := 0; i < t.NumField(); i++ { - result[i] = t.Field(i).Name + return out + case reflect.Array: + n := v.td.arrayLen + out := make([]R, n) + for i := 0; i < n; i++ { + out[i] = &rfx{ + td: v.td.Elem, + ptr: arrayElemAt(v.ptr, i, n, v.td.elemSize), + writable: v.writable, + } } - return result + return out } return nil } -// Raw 返回底层的 reflect.Value -func (r *rfx) Raw() reflect.Value { - return r.value +func (v *rfx) Keys() []string { + if !v.valid() { + return nil + } + if v.td.Kind == reflect.Struct { + out := make([]string, len(v.td.fields)) + for i := range v.td.fields { + out[i] = v.td.fields[i].Name + } + return out + } + return v.slow().Keys() } -// Lookup 根据路径查找并返回对应值的访问器 -func (r *rfx) Lookup(path string) (valuex.Accessor, bool) { - v := r.Get(path) - if v.Exists() { - return v, true +func (v *rfx) MarshalJSON() ([]byte, error) { + if !v.valid() { + return []byte("null"), nil + } + return json.Marshal(valueAt(v.td, v.ptr).Interface()) +} + +func (v *rfx) UnmarshalJSON(data []byte) error { + if !v.valid() { + return ErrInvalidValue + } + if !v.writable { + return NewErrCannotSetPath("") + } + return json.Unmarshal(data, ptrValueAt(v.td, v.ptr).Interface()) +} + +func (v *rfx) Lookup(path string) (valuex.Accessor, bool) { + got := v.Get(path) + if got.Exists() { + return got, true } return valuex.Nil, false } -// MustLookup 根据路径查找并直接返回对应值的访问器 -// 如果路径不存在,返回 valuex.Nil 访问器(所有方法返回零值) -func (r *rfx) MustLookup(path string) valuex.Accessor { - v := r.Get(path) - if v.Exists() { - return v +func (v *rfx) MustLookup(path string) valuex.Accessor { + got := v.Get(path) + if got.Exists() { + return got } return valuex.Nil } -// Ptr 返回指向当前值的指针 -func (r *rfx) Ptr() any { - v := r.value - if v.Kind() == reflect.Ptr { - return v.Interface() +// --------------------------------------------------------------------------- +// valuex.Accessor: 类型转换 +// --------------------------------------------------------------------------- + +func (v *rfx) Raw() reflect.Value { + if !v.valid() { + return reflect.Value{} } - if v.CanAddr() { - return v.Addr().Interface() + if v.ptrRoot { + // 与 reflux 对齐: New(&x) 的 Raw() 返回指针本身 + return ptrValueAt(v.td, v.ptr) } - return nil + return valueAt(v.td, v.ptr) } -// Any 将当前值转换为 any 类型 -func (r *rfx) Any() any { - v := r.value - for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { - if v.IsNil() { - return nil - } - v = v.Elem() - } - if !v.IsValid() { +func (v *rfx) Ptr() any { + if !v.valid() { return nil } - return v.Interface() + return ptrValueAt(v.td, v.ptr).Interface() } -// Bool 将当前值转换为 bool 类型 -func (r *rfx) Bool() bool { - result, err := cast.ToBoolE(r.Any()) +func (v *rfx) Any() any { + if !v.valid() { + return nil + } + cur, ok := (*v).normalize() + if !ok { + return nil + } + return valueAt(cur.td, cur.ptr).Interface() +} + +// --------------------------------------------------------------------------- +// 标量直读快路径 +// +// 目标类型与字段类型"内存宽度一致"时, 直接按 *T 读内存, 不经过 +// Any() -> interface 装箱 -> cast.ToXxxE 这条链路, 零分配。 +// +// 语义必须与 reflux(即 spf13/cast)完全一致, 已实测确认: +// - 整数溢出是**静默截断**, 不报错 (cast.ToInt8E(300) == 44, nil) +// - 负数转无符号才报错 (cast.ToUint8E(-1) -> error) +// 因此整数之间用 Go 原生转换即可对齐; 负数转无符号一律回退到 cast, 保证 panic 信息一致。 +// +// 浮点字段读成窄整数(Int8/Int16/Int32)不走快路径 —— Go 里超范围的 float->int +// 转换结果是实现定义的, 与 cast 的直接转换未必一致, 这种边角情况交给 cast。 +// --------------------------------------------------------------------------- + +// scalarInt64 把整数/bool 类型的字段直接读成 int64。 +// ok 为 false 表示当前类型没有快路径, 调用方需要回退到 cast。 +func (v rfx) scalarInt64() (int64, bool) { + switch v.td.Kind { + case reflect.Int: + return int64(loadInt(v.ptr)), true + case reflect.Int8: + return int64(loadInt8(v.ptr)), true + case reflect.Int16: + return int64(loadInt16(v.ptr)), true + case reflect.Int32: + return int64(loadInt32(v.ptr)), true + case reflect.Int64: + return loadInt64(v.ptr), true + case reflect.Uint: + return int64(loadUint(v.ptr)), true + case reflect.Uint8: + return int64(loadUint8(v.ptr)), true + case reflect.Uint16: + return int64(loadUint16(v.ptr)), true + case reflect.Uint32: + return int64(loadUint32(v.ptr)), true + case reflect.Bool: + if loadBool(v.ptr) { + return 1, true + } + return 0, true + } + return 0, false +} + +// scalarUint64 把整数/bool 类型的字段直接读成 uint64。 +// 负数一律返回 ok=false 回退到 cast, 这样 panic 信息与 reflux 完全一致。 +func (v rfx) scalarUint64() (uint64, bool) { + switch v.td.Kind { + case reflect.Uint: + return uint64(loadUint(v.ptr)), true + case reflect.Uint8: + return uint64(loadUint8(v.ptr)), true + case reflect.Uint16: + return uint64(loadUint16(v.ptr)), true + case reflect.Uint32: + return uint64(loadUint32(v.ptr)), true + case reflect.Uint64: + return loadUint64(v.ptr), true + // 注意: 这里**故意不处理** reflect.Uintptr。 + // spf13/cast 不支持 uintptr, reflux 读 uintptr 字段是 panic 的; + // 快路径若把它算进来就会返回数值, 与现状不一致。 + // 由 TestFullInterfaceContract 的 "uintptr 字段读 Uint64" 用例钉住。 + case reflect.Bool: + if loadBool(v.ptr) { + return 1, true + } + return 0, true + } + if n, ok := v.scalarInt64(); ok && n >= 0 { + return uint64(n), true + } + return 0, false +} + +// scalarFloat64 把数值类型的字段直接读成 float64。 +func (v rfx) scalarFloat64() (float64, bool) { + switch v.td.Kind { + case reflect.Float64: + return loadFloat64(v.ptr), true + case reflect.Float32: + return float64(loadFloat32(v.ptr)), true + case reflect.Uint64: + return float64(loadUint64(v.ptr)), true + } + if n, ok := v.scalarInt64(); ok { + return float64(n), true + } + return 0, false +} + +// isFloatKind 报告字段本身是不是浮点类型。 +func (v rfx) isFloatKind() bool { + return v.td.Kind == reflect.Float32 || v.td.Kind == reflect.Float64 +} + +// String 对 string 类型有零分配快路径: 直接按 *string 读, 不经过 +// interface 装箱和 cast。其余类型与 reflux 行为一致。 +func (v *rfx) String() string { + if v.valid() { + // 数字/布尔转字符串也走直读 + strconv, 绕开 interface 装箱。 + // 格式必须与 cast.ToStringE 完全一致, 由 TestScalarFastPathMatchesReflux + // 的全矩阵对拍保证。 + switch v.td.Kind { + case reflect.String: + return loadString(v.ptr) + case reflect.Bool: + return strconv.FormatBool(loadBool(v.ptr)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, _ := (*v).scalarInt64() + return strconv.FormatInt(n, 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, reflect.Uintptr: + u, _ := (*v).scalarUint64() + return strconv.FormatUint(u, 10) + case reflect.Float64: + return strconv.FormatFloat(loadFloat64(v.ptr), 'f', -1, 64) + case reflect.Float32: + return strconv.FormatFloat(float64(loadFloat32(v.ptr)), 'f', -1, 32) + } + } + data := v.Any() + if data == nil { + return "" + } + result, err := cast.ToStringE(data) + if err != nil { + if b, e := json.Marshal(data); e == nil { + return string(b) + } + panic(fmt.Sprintf("rfx: failed to convert to string: %v", err)) + } + return result +} + +func (v *rfx) Bool() bool { + if v.valid() && v.td.Kind == reflect.Bool { + return loadBool(v.ptr) + } + result, err := cast.ToBoolE(v.Any()) if err != nil { panic(fmt.Sprintf("rfx: failed to convert to bool: %v", err)) } return result } -// Float64 将当前值转换为 float64 类型 -func (r *rfx) Float64() float64 { - result, err := cast.ToFloat64E(r.Any()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to float64: %v", err)) +func (v *rfx) Int() int { + if v.valid() { + if n, ok := (*v).scalarInt64(); ok { + return int(n) + } } - return result -} - -// Float32 将当前值转换为 float32 类型 -func (r *rfx) Float32() float32 { - result, err := cast.ToFloat32E(r.Any()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to float32: %v", err)) - } - return result -} - -// Int64 将当前值转换为 int64 类型 -func (r *rfx) Int64() int64 { - result, err := cast.ToInt64E(r.Any()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to int64: %v", err)) - } - return result -} - -// Int32 将当前值转换为 int32 类型 -func (r *rfx) Int32() int32 { - result, err := cast.ToInt32E(r.Any()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to int32: %v", err)) - } - return result -} - -// Int16 将当前值转换为 int16 类型 -func (r *rfx) Int16() int16 { - result, err := cast.ToInt16E(r.Any()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to int16: %v", err)) - } - return result -} - -// Int8 将当前值转换为 int8 类型 -func (r *rfx) Int8() int8 { - result, err := cast.ToInt8E(r.Any()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to int8: %v", err)) - } - return result -} - -// Int 将当前值转换为 int 类型 -func (r *rfx) Int() int { - result, err := cast.ToIntE(r.Any()) + result, err := cast.ToIntE(v.Any()) if err != nil { panic(fmt.Sprintf("rfx: failed to convert to int: %v", err)) } return result } -// Uint 将当前值转换为 uint 类型 -func (r *rfx) Uint() uint { - result, err := cast.ToUintE(r.Any()) +func (v *rfx) Int64() int64 { + if v.valid() { + if n, ok := (*v).scalarInt64(); ok { + return n + } + } + result, err := cast.ToInt64E(v.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int64: %v", err)) + } + return result +} + +// Int32/Int16/Int8 只在源本身是整数/bool 时走快路径。 +// 浮点源交给 cast —— 超范围的 float->窄整数 转换在 Go 里是实现定义的。 +func (v *rfx) Int32() int32 { + if v.valid() && !v.isFloatKind() { + if n, ok := (*v).scalarInt64(); ok { + return int32(n) + } + } + result, err := cast.ToInt32E(v.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int32: %v", err)) + } + return result +} + +func (v *rfx) Int16() int16 { + if v.valid() && !v.isFloatKind() { + if n, ok := (*v).scalarInt64(); ok { + return int16(n) + } + } + result, err := cast.ToInt16E(v.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int16: %v", err)) + } + return result +} + +func (v *rfx) Int8() int8 { + if v.valid() && !v.isFloatKind() { + if n, ok := (*v).scalarInt64(); ok { + return int8(n) + } + } + result, err := cast.ToInt8E(v.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int8: %v", err)) + } + return result +} + +func (v *rfx) Uint() uint { + if v.valid() { + if n, ok := (*v).scalarUint64(); ok { + return uint(n) + } + } + result, err := cast.ToUintE(v.Any()) if err != nil { panic(fmt.Sprintf("rfx: failed to convert to uint: %v", err)) } return result } -// Uint64 将当前值转换为 uint64 类型 -func (r *rfx) Uint64() uint64 { - result, err := cast.ToUint64E(r.Any()) +func (v *rfx) Uint64() uint64 { + if v.valid() { + if n, ok := (*v).scalarUint64(); ok { + return n + } + } + result, err := cast.ToUint64E(v.Any()) if err != nil { panic(fmt.Sprintf("rfx: failed to convert to uint64: %v", err)) } return result } -// Uint32 将当前值转换为 uint32 类型 -func (r *rfx) Uint32() uint32 { - result, err := cast.ToUint32E(r.Any()) +func (v *rfx) Uint32() uint32 { + if v.valid() && !v.isFloatKind() { + if n, ok := (*v).scalarUint64(); ok { + return uint32(n) + } + } + result, err := cast.ToUint32E(v.Any()) if err != nil { panic(fmt.Sprintf("rfx: failed to convert to uint32: %v", err)) } return result } -// Uint16 将当前值转换为 uint16 类型 -func (r *rfx) Uint16() uint16 { - result, err := cast.ToUint16E(r.Any()) +func (v *rfx) Uint16() uint16 { + if v.valid() && !v.isFloatKind() { + if n, ok := (*v).scalarUint64(); ok { + return uint16(n) + } + } + result, err := cast.ToUint16E(v.Any()) if err != nil { panic(fmt.Sprintf("rfx: failed to convert to uint16: %v", err)) } return result } -// Uint8 将当前值转换为 uint8 类型 -func (r *rfx) Uint8() uint8 { - result, err := cast.ToUint8E(r.Any()) +func (v *rfx) Uint8() uint8 { + if v.valid() && !v.isFloatKind() { + if n, ok := (*v).scalarUint64(); ok { + return uint8(n) + } + } + result, err := cast.ToUint8E(v.Any()) if err != nil { panic(fmt.Sprintf("rfx: failed to convert to uint8: %v", err)) } return result } -// String 将当前值转换为 string 类型 -func (r *rfx) String() string { - data := r.Any() - result, err := cast.ToStringE(data) +func (v *rfx) Float64() float64 { + if v.valid() { + if f, ok := (*v).scalarFloat64(); ok { + return f + } + } + result, err := cast.ToFloat64E(v.Any()) if err != nil { - if b, e := json.Marshal(data); e == nil { - result = string(b) - } else { - panic(fmt.Sprintf("rfx: failed to convert to string: %v", err)) - } + panic(fmt.Sprintf("rfx: failed to convert to float64: %v", err)) } return result } -// StringMapString 将当前值转换为 map[string]string 类型 -func (r *rfx) StringMapString() map[string]string { - data := r.Any() - - // 首先尝试使用 cast 进行转换 - result, err := cast.ToStringMapStringE(data) - if err == nil { - return result - } - - // cast 失败后,尝试自定义转换逻辑 - defer func() { - if p := recover(); p != nil { - panic(fmt.Sprintf("rfx: failed to convert to map[string]string, cast error: %v, custom conversion error: %v", err, p)) - } - }() - - result = make(map[string]string) - for _, k := range r.Keys() { - result[k] = r.Get(k).String() - } - - return result -} - -// StringMapStringSlice 将当前值转换为 map[string][]string 类型 -func (r *rfx) StringMapStringSlice() map[string][]string { - data := r.Any() - - // 首先尝试使用 cast 进行转换 - result, err := cast.ToStringMapStringSliceE(data) - if err == nil { - return result - } - - // cast 失败后,尝试自定义转换逻辑 - defer func() { - if p := recover(); p != nil { - panic(fmt.Sprintf("rfx: failed to convert to map[string][]string, cast error: %v, custom conversion error: %v", err, p)) - } - }() - - result = make(map[string][]string) - for _, k := range r.Keys() { - result[k] = r.Get(k).StringSlice() - } - - return result -} - -// StringMapBool 将当前值转换为 map[string]bool 类型 -func (r *rfx) StringMapBool() map[string]bool { - data := r.Any() - - // 首先尝试使用 cast 进行转换 - result, err := cast.ToStringMapBoolE(data) - if err == nil { - return result - } - - // cast 失败后,尝试自定义转换逻辑 - defer func() { - if p := recover(); p != nil { - panic(fmt.Sprintf("rfx: failed to convert to map[string]bool, cast error: %v, custom conversion error: %v", err, p)) - } - }() - - result = make(map[string]bool) - for _, k := range r.Keys() { - result[k] = r.Get(k).Bool() - } - - return result -} - -// StringMapInt 将当前值转换为 map[string]int 类型 -func (r *rfx) StringMapInt() map[string]int { - data := r.Any() - - // 首先尝试使用 cast 进行转换 - result, err := cast.ToStringMapIntE(data) - if err == nil { - return result - } - - // cast 失败后,尝试自定义转换逻辑 - defer func() { - if p := recover(); p != nil { - panic(fmt.Sprintf("rfx: failed to convert to map[string]int, cast error: %v, custom conversion error: %v", err, p)) - } - }() - - result = make(map[string]int) - for _, k := range r.Keys() { - result[k] = r.Get(k).Int() - } - - return result -} - -// StringMapInt64 将当前值转换为 map[string]int64 类型 -func (r *rfx) StringMapInt64() map[string]int64 { - data := r.Any() - - // 首先尝试使用 cast 进行转换 - result, err := cast.ToStringMapInt64E(data) - if err == nil { - return result - } - - // cast 失败后,尝试自定义转换逻辑 - defer func() { - if p := recover(); p != nil { - panic(fmt.Sprintf("rfx: failed to convert to map[string]int64, cast error: %v, custom conversion error: %v", err, p)) - } - }() - - result = make(map[string]int64) - for _, k := range r.Keys() { - result[k] = r.Get(k).Int64() - } - - return result -} - -// StringMap 将当前值转换为 map[string]any 类型 -func (r *rfx) StringMap() map[string]any { - data := r.Any() - - // 首先尝试使用 cast 进行转换 - result, err := cast.ToStringMapE(data) - if err == nil { - return result - } - - // cast 失败后,尝试自定义转换逻辑 - defer func() { - if p := recover(); p != nil { - panic(fmt.Sprintf("rfx: failed to convert to map[string]any, cast error: %v, custom conversion error: %v", err, p)) - } - }() - - result = make(map[string]any) - for _, k := range r.Keys() { - result[k] = r.Get(k).Any() - } - - return result -} - -// Slice 将当前值转换为 []any 切片 -func (r *rfx) Slice() (result []any) { - v := r.value - for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { - if v.IsNil() { - return nil - } - v = v.Elem() - } - - if !v.IsValid() { - return nil - } else if v.Kind() == reflect.String { - if str := r.String(); len(str) >= 2 && str[0] == '[' { - if err := json.Unmarshal([]byte(str), &result); err == nil { - return result - } - } - } else if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { - return nil - } - - result = make([]any, v.Len()) - for i := 0; i < v.Len(); i++ { - elem := v.Index(i) - for elem.Kind() == reflect.Ptr || elem.Kind() == reflect.Interface { - if elem.IsNil() { - break - } - elem = elem.Elem() - } - if elem.IsValid() { - result[i] = elem.Interface() +func (v *rfx) Float32() float32 { + if v.valid() { + if f, ok := (*v).scalarFloat64(); ok { + return float32(f) } } - return result -} - -// BoolSlice 将当前值转换为 []bool 切片 -func (r *rfx) BoolSlice() []bool { - result, err := cast.ToBoolSliceE(r.Slice()) + result, err := cast.ToFloat32E(v.Any()) if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to []bool: %v", err)) + panic(fmt.Sprintf("rfx: failed to convert to float32: %v", err)) } return result } -// StringSlice 将当前值转换为 []string 切片 -func (r *rfx) StringSlice() []string { - result, err := cast.ToStringSliceE(r.Slice()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to []string: %v", err)) - } - return result -} - -// IntSlice 将当前值转换为 []int 切片 -func (r *rfx) IntSlice() []int { - result, err := cast.ToIntSliceE(r.Slice()) - if err != nil { - panic(fmt.Sprintf("rfx: failed to convert to []int: %v", err)) - } - return result -} - -// MarshalJSON 实现 json.Marshaler 接口 -// 将当前值序列化为 JSON 字节数组 -func (r *rfx) MarshalJSON() ([]byte, error) { - return json.Marshal(r.Any()) -} - -// UnmarshalJSON 实现 json.Unmarshaler 接口 -// 从 JSON 字节数组反序列化到当前值 -func (r *rfx) UnmarshalJSON(data []byte) error { - // 先解析到 any 类型 - var v any - if err := json.Unmarshal(data, &v); err != nil { - return err - } - - // 获取当前值的实际类型 - target := r.value - for target.Kind() == reflect.Ptr || target.Kind() == reflect.Interface { - if target.IsNil() { - break - } - target = target.Elem() - } - - // 如果当前值无效或为 nil,创建一个新的 map[string]any - if !target.IsValid() || !target.CanSet() { - r.value = reflect.ValueOf(&v).Elem() - return nil - } - - // 尝试将解析的值设置到当前值 - newValue := reflect.ValueOf(v) - if newValue.Type().AssignableTo(target.Type()) { - target.Set(newValue) - return nil - } - - // 如果类型不匹配,尝试使用 setValue 进行转换 - if err := r.setValue(target, v); err != nil { - return NewErrUnmarshalFailed(target.Type(), err) - } - - return nil -} +func (v *rfx) StringMapString() map[string]string { return v.slow().StringMapString() } +func (v *rfx) StringMapStringSlice() map[string][]string { return v.slow().StringMapStringSlice() } +func (v *rfx) StringMapBool() map[string]bool { return v.slow().StringMapBool() } +func (v *rfx) StringMapInt() map[string]int { return v.slow().StringMapInt() } +func (v *rfx) StringMapInt64() map[string]int64 { return v.slow().StringMapInt64() } +func (v *rfx) StringMap() map[string]any { return v.slow().StringMap() } +func (v *rfx) Slice() []any { return v.slow().Slice() } +func (v *rfx) BoolSlice() []bool { return v.slow().BoolSlice() } +func (v *rfx) StringSlice() []string { return v.slow().StringSlice() } +func (v *rfx) IntSlice() []int { return v.slow().IntSlice() } diff --git a/rfx_alias_test.go b/rfx_alias_test.go new file mode 100644 index 0000000..5dadca2 --- /dev/null +++ b/rfx_alias_test.go @@ -0,0 +1,92 @@ +package reflux + +// 链式 Get 传的是地址而不是数据副本 —— 这一点决定了每次 Get 那 1 次分配 +// 只是 24 字节的游标, 与被访问数据的大小无关。 + +import "testing" + +// 链式 Get 到底是"传地址"还是"每一跳复制一份数据"? +// +// 结论: 传地址。每次 Get 在堆上新建的只是一个 24 字节的游标(类型描述符指针 + +// 目标地址 + 可写标记), 它的 ptr 字段直接指向原对象内部, 被访问的数据一个字节 +// 都没有被复制。 +func TestChainedGetSharesAddress(t *testing.T) { + o := newUxOuter() + r := New(o) + + // 原始字段的真实地址 + want := &o.Mid.Inner.City + + oneShot := r.Get("Mid", "Inner", "City").Ptr().(*string) + dotted := r.Get("Mid.Inner.City").Ptr().(*string) + chained := r.Get("Mid").Get("Inner").Get("City").Ptr().(*string) + + if oneShot != want { + t.Fatalf("一次调用拿到的不是原字段地址: %p vs %p", oneShot, want) + } + if dotted != want { + t.Fatalf("点号路径拿到的不是原字段地址: %p vs %p", dotted, want) + } + if chained != want { + t.Fatalf("链式调用拿到的不是原字段地址: %p vs %p", chained, want) + } + + // 中间跳也是原对象内部的地址, 不是副本 + mid := r.Get("Mid").Ptr().(*uxMiddle) + if mid != &o.Mid { + t.Fatalf("中间跳 Mid 不是原地址: %p vs %p", mid, &o.Mid) + } + uxInner := r.Get("Mid").Get("Inner").Ptr().(*uxInner) + if uxInner != &o.Mid.Inner { + t.Fatalf("中间跳 Inner 不是原地址: %p vs %p", uxInner, &o.Mid.Inner) + } + + // 经链式写入, 必须落到原对象上 + r.Get("Mid").Get("Inner").Set("City", "WrittenViaChain") + if o.Mid.Inner.City != "WrittenViaChain" { + t.Fatalf("链式写入没落到原对象: %q", o.Mid.Inner.City) + } +} + +// reflux 现有实现同样是地址传递(reflect.Value 内部存的也是字段地址), +// 两边在这一点上语义一致。 +func TestRefluxAlsoSharesAddress(t *testing.T) { + o := newUxOuter() + r := newLegacy(o) + + want := &o.Mid.Inner.City + got := r.Get("Mid").Get("Inner").Get("City").Ptr().(*string) + if got != want { + t.Fatalf("reflux 链式拿到的不是原字段地址: %p vs %p", got, want) + } +} + +// 唯一会复制的两种情况: map 取值 和 interface 拆出非指针值。 +// 它们在 Go 里本来就不可寻址, reflect 也一样只能给副本。 +func TestMapAndIfaceAreCopies(t *testing.T) { + o := newUxOuter() + r := New(o) + + // map: 两次取同一个键, 拿到两个不同的地址 => 是副本 + a := r.Get("Objs", "o1").Ptr().(*uxInner) + b := r.Get("Objs", "o1").Ptr().(*uxInner) + if a == b { + t.Fatal("map 取值应该每次给一份独立副本") + } + if a.City != "Chengdu" || b.City != "Chengdu" { + t.Fatalf("副本内容不对: %q %q", a.City, b.City) + } + + // interface 里装的是非指针值, 同样是副本 + c := r.Get("Mid", "Any").Ptr().(*uxInner) + d := r.Get("Mid", "Any").Ptr().(*uxInner) + if c == d { + t.Fatal("interface 拆非指针值应该每次给一份独立副本") + } + + // 但 slice 元素是原地址(底层数组), 不是副本 + e := r.Get("Structs", "0").Ptr().(*uxInner) + if e != &o.Structs[0] { + t.Fatalf("slice 元素应该是原地址: %p vs %p", e, &o.Structs[0]) + } +} diff --git a/rfx_bench_test.go b/rfx_bench_test.go new file mode 100644 index 0000000..9e8da2c --- /dev/null +++ b/rfx_bench_test.go @@ -0,0 +1,294 @@ +package reflux + +// 新实现(rfx: 类型布局缓存 + unsafe 指针运算)与老实现(refx: 逐次 reflect)的对比。 +// +// 两者在同一个包、同一次进程里跑, 用完全相同的数据结构和路径, 结果可直接比较。 +// newLegacy 构造的就是合并前的实现, 见 rfx_contract_test.go。 + +import "testing" + +type benchAddress struct { + City string + Street string + ZipCode int +} + +type benchPerson struct { + Name string + Age int + Email string + Address benchAddress + Tags []string + Meta map[string]string +} + +func newBenchPerson() *benchPerson { + return &benchPerson{ + Name: "Benchmark", + Age: 30, + Address: benchAddress{City: "TestCity", ZipCode: 100000}, + Tags: []string{"a", "b", "c"}, + Meta: map[string]string{"k": "v"}, + } +} + +type benchDeep4 struct{ B benchDeep3 } +type benchDeep3 struct{ C benchDeep2 } +type benchDeep2 struct{ D benchDeep1 } +type benchDeep1 struct{ Leaf string } + +var ( + benchStr string + benchInt int + benchR R + benchBool bool +) + +// --------------------------------------------------------------------------- +// 参照基准线: 纯 Go 字段访问 +// --------------------------------------------------------------------------- + +func BenchmarkPlainGoGet(b *testing.B) { + p := newBenchPerson() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = p.Address.City + } +} + +// --------------------------------------------------------------------------- +// Get: 嵌套字段 +// --------------------------------------------------------------------------- + +func BenchmarkGetLegacy(b *testing.B) { + r := newLegacy(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Address", "City").String() + } +} + +func BenchmarkGetNew(b *testing.B) { + r := New(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Address", "City").String() + } +} + +func BenchmarkGetGeneric(b *testing.B) { + r := New(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = Get[string](r, "Address", "City") + } +} + +// 点号路径 +func BenchmarkGetDottedLegacy(b *testing.B) { + r := newLegacy(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Address.City").String() + } +} + +func BenchmarkGetDottedNew(b *testing.B) { + r := New(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Address.City").String() + } +} + +// 4 层深路径 +func BenchmarkGetDeepLegacy(b *testing.B) { + r := newLegacy(&benchDeep4{B: benchDeep3{C: benchDeep2{D: benchDeep1{Leaf: "deep"}}}}) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("B", "C", "D", "Leaf").String() + } +} + +func BenchmarkGetDeepNew(b *testing.B) { + r := New(&benchDeep4{B: benchDeep3{C: benchDeep2{D: benchDeep1{Leaf: "deep"}}}}) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("B", "C", "D", "Leaf").String() + } +} + +func BenchmarkGetDeepGeneric(b *testing.B) { + r := New(&benchDeep4{B: benchDeep3{C: benchDeep2{D: benchDeep1{Leaf: "deep"}}}}) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = Get[string](r, "B", "C", "D", "Leaf") + } +} + +// slice 下标 +func BenchmarkGetSliceIndexLegacy(b *testing.B) { + r := newLegacy(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Tags", "1").String() + } +} + +func BenchmarkGetSliceIndexNew(b *testing.B) { + r := New(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Tags", "1").String() + } +} + +// map 键 +func BenchmarkGetMapKeyLegacy(b *testing.B) { + r := newLegacy(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Meta", "k").String() + } +} + +func BenchmarkGetMapKeyNew(b *testing.B) { + r := New(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = r.Get("Meta", "k").String() + } +} + +// --------------------------------------------------------------------------- +// Set +// --------------------------------------------------------------------------- + +func BenchmarkSetLegacy(b *testing.B) { + r := newLegacy(&benchPerson{}) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Set("Name", "TestName") + } +} + +func BenchmarkSetNew(b *testing.B) { + r := New(&benchPerson{}) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Set("Name", "TestName") + } +} + +func BenchmarkSetNestedLegacy(b *testing.B) { + r := newLegacy(&benchPerson{}) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Set("Address.City", "TestCity") + } +} + +func BenchmarkSetNestedNew(b *testing.B) { + r := New(&benchPerson{}) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Set("Address.City", "TestCity") + } +} + +// --------------------------------------------------------------------------- +// 访问器: 纯类型转换开销(循环外先取到目标值) +// --------------------------------------------------------------------------- + +func BenchmarkAccessorStringLegacy(b *testing.B) { + v := newLegacy(newBenchPerson()).Get("Address.City") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = v.String() + } +} + +func BenchmarkAccessorStringNew(b *testing.B) { + v := New(newBenchPerson()).Get("Address.City") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchStr = v.String() + } +} + +func BenchmarkAccessorIntLegacy(b *testing.B) { + v := newLegacy(newBenchPerson()).Get("Age") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchInt = v.Int() + } +} + +func BenchmarkAccessorIntNew(b *testing.B) { + v := New(newBenchPerson()).Get("Age") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchInt = v.Int() + } +} + +// --------------------------------------------------------------------------- +// 其它 API +// --------------------------------------------------------------------------- + +func BenchmarkExistsLegacy(b *testing.B) { + r := newLegacy(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchBool = r.Exists("Address", "City") + } +} + +func BenchmarkExistsNew(b *testing.B) { + r := New(newBenchPerson()) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchBool = r.Exists("Address", "City") + } +} + +func BenchmarkNewPtrLegacy(b *testing.B) { + p := newBenchPerson() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchR = newLegacy(p) + } +} + +func BenchmarkNewPtrNew(b *testing.B) { + p := newBenchPerson() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchR = New(p) + } +} diff --git a/rfx_contract_test.go b/rfx_contract_test.go new file mode 100644 index 0000000..d86c2a7 --- /dev/null +++ b/rfx_contract_test.go @@ -0,0 +1,253 @@ +package reflux + +// 本文件里的测试用**老的 reflect 实现 refx 作为参照**, 逐项对拍新实现 rfx。 +// +// 这么做的价值在于: refx 现在仍然作为冷路径存在于包内, 所以这套差分测试 +// 可以长期保留 —— 任何时候改动 rfx 的快路径, 都能立刻和参照实现比出差异。 +// 合并过程中 11 个真实的行为偏差就是这么发现的。 + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "git.fsdpf.net/go/reflux/valuex" +) + +// newLegacy 用老的 reflect 实现构造 R, 逻辑与合并前的 New 完全一致。 +func newLegacy(v any) R { + switch t := v.(type) { + case nil: + return Nil + case R: + return t + case []R: + return &refx{value: reflect.ValueOf(t)} + default: + if t == valuex.Nil { + return Nil + } + } + + rv, isPtr, err := normalizeInputValue(v) + if err != nil { + panic(err) + } + if !rv.IsValid() { + panic(ErrInvalidValue) + } + + actualValue := rv + for actualValue.Kind() == reflect.Ptr || actualValue.Kind() == reflect.Interface { + if actualValue.IsNil() { + panic(ErrTargetNilPointer) + } + actualValue = actualValue.Elem() + if actualValue.Kind() == reflect.Ptr { + isPtr = true + } + } + + switch actualValue.Kind() { + case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array, + reflect.String, reflect.Bool, reflect.Float32, reflect.Float64: + default: + panic(NewErrUnsupportedTargetType(actualValue.Kind())) + } + + if !isPtr { + rv = DeepClone(actualValue) + } + return &refx{value: rv} +} + +// R 的每一个方法都在这里与 reflux 对拍一遍。 +// 前面的 compat 套件只能覆盖 reflux 自己测到的部分(45.2%), 这个文件补齐 +// 整个接口契约, 回答"是不是真的全兼容"。 + +type contractDoc struct { + Title string + Count int + Ratio float64 + Ok bool + Ptr *string + UPtr uintptr + I8 int8 + I16 int16 + I32 int32 + U uint + U8 uint8 + U16 uint16 + U32 uint32 + U64 uint64 + F32 float32 + Tags []string + Nums []int + Flags []bool + Anys []any + SMap map[string]string + IMap map[string]int + I64Map map[string]int64 + BMap map[string]bool + AMap map[string]any + SSMap map[string][]string + Sub struct{ A, B string } +} + +func newContractDoc() *contractDoc { + s := "ptr" + return &contractDoc{ + Title: "doc", Count: 7, Ratio: 2.5, Ok: true, Ptr: &s, UPtr: 12, + I8: 8, I16: 16, I32: 32, U: 1, U8: 2, U16: 3, U32: 4, U64: 5, F32: 1.25, + Tags: []string{"a", "b"}, + Nums: []int{1, 2, 3}, + Flags: []bool{true, false}, + Anys: []any{"x", 2, true}, + SMap: map[string]string{"k": "v"}, + IMap: map[string]int{"k": 1}, + I64Map: map[string]int64{"k": 2}, + BMap: map[string]bool{"k": true}, + AMap: map[string]any{"k": "v"}, + SSMap: map[string][]string{"k": {"a", "b"}}, + Sub: struct{ A, B string }{"a", "b"}, + } +} + +// call 调用一个方法, 把结果格式化成可比较的字符串。 +// +// panic 也算一种结果, 而且**连错误信息一起比对** —— 错误文案属于行为契约的一部分, +// 之前 "set failed at path 'Items.10': set failed at path '10': ..." 那次双重包装 +// 就是只比对"有没有 panic"发现不了的。 +func call(r R, f func(R) any) string { + var out string + func() { + defer func() { + if rec := recover(); rec != nil { + out = "PANIC: " + fmt.Sprintf("%v", rec) + } + }() + out = fmt.Sprintf("%v", f(r)) + }() + return out +} + +// TestFullInterfaceContract 遍历 R 的全部方法, 逐个与 reflux 对拍。 +func TestFullInterfaceContract(t *testing.T) { + cases := []struct { + name string + f func(R) any + }{ + // --- 路径与结构 --- + {"Get 单层", func(r R) any { return r.Get("Title").String() }}, + {"Get 嵌套", func(r R) any { return r.Get("Sub", "A").String() }}, + {"Get 点号", func(r R) any { return r.Get("Sub.B").String() }}, + {"Get 指针字段", func(r R) any { return r.Get("Ptr").String() }}, + {"Get slice 下标", func(r R) any { return r.Get("Tags", "1").String() }}, + {"Get map 键", func(r R) any { return r.Get("SMap", "k").String() }}, + {"Get 不存在", func(r R) any { return r.Get("Nope").String() }}, + {"Exists 真", func(r R) any { return r.Exists("Sub", "A") }}, + {"Exists 假", func(r R) any { return r.Exists("Nope") }}, + {"Keys struct", func(r R) any { return r.Keys() }}, + {"Keys map", func(r R) any { return r.Get("SMap").Keys() }}, + {"Array slice", func(r R) any { return len(r.Get("Tags").Array()) }}, + {"Array 元素值", func(r R) any { return r.Get("Tags").Array()[0].String() }}, + {"Scope", func(r R) any { return r.Scope("Sub").Get("A").String() }}, + + // --- Lookuper (reflux 自己的测试完全没覆盖) --- + {"Lookup 命中", func(r R) any { + a, ok := r.Lookup("Sub.A") + return fmt.Sprintf("%v/%v", a.String(), ok) + }}, + {"Lookup 未命中", func(r R) any { + a, ok := r.Lookup("Nope") + return fmt.Sprintf("%v/%v", a.String(), ok) + }}, + {"MustLookup 命中", func(r R) any { return r.MustLookup("Sub.A").String() }}, + {"MustLookup 未命中", func(r R) any { return r.MustLookup("Nope").String() }}, + + // --- 标量访问器(全宽度) --- + {"String", func(r R) any { return r.Get("Title").String() }}, + {"Bool", func(r R) any { return r.Get("Ok").Bool() }}, + {"Int", func(r R) any { return r.Get("Count").Int() }}, + {"Int8", func(r R) any { return r.Get("I8").Int8() }}, + {"Int16", func(r R) any { return r.Get("I16").Int16() }}, + {"Int32", func(r R) any { return r.Get("I32").Int32() }}, + {"Int64", func(r R) any { return r.Get("Count").Int64() }}, + {"Uint", func(r R) any { return r.Get("U").Uint() }}, + {"Uint8", func(r R) any { return r.Get("U8").Uint8() }}, + {"Uint16", func(r R) any { return r.Get("U16").Uint16() }}, + {"Uint32", func(r R) any { return r.Get("U32").Uint32() }}, + {"Uint64", func(r R) any { return r.Get("U64").Uint64() }}, + {"Float32", func(r R) any { return r.Get("F32").Float32() }}, + {"Float64", func(r R) any { return r.Get("Ratio").Float64() }}, + {"uintptr 字段读 Uint64", func(r R) any { return r.Get("UPtr").Uint64() }}, + {"Any", func(r R) any { return r.Get("Count").Any() }}, + {"Raw Kind", func(r R) any { return r.Get("Title").Raw().Kind().String() }}, + {"Raw Kind 根", func(r R) any { return r.Raw().Kind().String() }}, + {"Ptr 类型", func(r R) any { return fmt.Sprintf("%T", r.Get("Title").Ptr()) }}, + + // --- 容器转换(StringMap / StringMapInt64 两边都没测过) --- + {"StringMapString", func(r R) any { return r.Get("SMap").StringMapString() }}, + {"StringMapInt", func(r R) any { return r.Get("IMap").StringMapInt() }}, + {"StringMapInt64", func(r R) any { return r.Get("I64Map").StringMapInt64() }}, + {"StringMapBool", func(r R) any { return r.Get("BMap").StringMapBool() }}, + {"StringMap", func(r R) any { return r.Get("AMap").StringMap() }}, + {"StringMapStringSlice", func(r R) any { return r.Get("SSMap").StringMapStringSlice() }}, + {"Slice", func(r R) any { return r.Get("Anys").Slice() }}, + {"StringSlice", func(r R) any { return r.Get("Tags").StringSlice() }}, + {"IntSlice", func(r R) any { return r.Get("Nums").IntSlice() }}, + {"BoolSlice", func(r R) any { return r.Get("Flags").BoolSlice() }}, + + // --- JSON --- + {"MarshalJSON", func(r R) any { + b, err := json.Marshal(r.Get("Sub")) + return fmt.Sprintf("%s/%v", b, err) + }}, + + // --- 写操作 --- + {"Set 标量", func(r R) any { r.Set("Title", "x"); return r.Get("Title").String() }}, + {"Set 嵌套", func(r R) any { r.Set("Sub.A", "y"); return r.Get("Sub.A").String() }}, + {"Set 跨类型", func(r R) any { r.Set("Count", "42"); return r.Get("Count").Int() }}, + {"Set 指针字段", func(r R) any { r.Set("Ptr", "z"); return r.Get("Ptr").String() }}, + {"Set slice 元素", func(r R) any { r.Set("Tags.0", "q"); return r.Get("Tags.0").String() }}, + {"Set map 键", func(r R) any { r.Set("SMap.k", "w"); return r.Get("SMap.k").String() }}, + {"Set 新 map 键", func(r R) any { r.Set("SMap.new", "n"); return r.Get("SMap.new").String() }}, + {"Set 切片整体", func(r R) any { r.Set("Tags", []string{"p"}); return r.Get("Tags").StringSlice() }}, + {"Set []any 转切片", func(r R) any { r.Set("Nums", []any{9, 8}); return r.Get("Nums").IntSlice() }}, + {"Set 不存在字段", func(r R) any { r.Set("Nope", 1); return "no-panic" }}, + {"Set slice 越界", func(r R) any { r.Set("Tags.99", "x"); return "no-panic" }}, + {"Append", func(r R) any { r.Get("Tags").Append("c"); return r.Get("Tags").StringSlice() }}, + {"Delete map 键", func(r R) any { r.Get("SMap").Delete("k"); return r.Get("SMap").Keys() }}, + // --- 失败路径: 错误信息必须逐字一致 --- + {"Set 复合目标类型不符", func(r R) any { r.Set("Sub", 42); return "no-panic" }}, + {"Set 切片目标类型不符", func(r R) any { r.Set("Tags", 42); return "no-panic" }}, + {"Set map 目标类型不符", func(r R) any { r.Set("SMap", 42); return "no-panic" }}, + {"Set 嵌套路径不存在", func(r R) any { r.Set("Sub.Nope", "x"); return "no-panic" }}, + {"Set 空路径", func(r R) any { r.Set("", 1); return "no-panic" }}, + {"Set 未导出字段", func(r R) any { r.Set("unexported", 1); return "no-panic" }}, + {"Set 到标量的子路径", func(r R) any { r.Set("Title.X", 1); return "no-panic" }}, + {"Int 无法转换", func(r R) any { return r.Get("Title").Int() }}, + {"Uint 负数", func(r R) any { r.Set("Count", -1); return r.Get("Count").Uint8() }}, + {"Bool 无法转换", func(r R) any { return r.Get("Title").Bool() }}, + {"Append 到非切片", func(r R) any { r.Get("Title").Append("x"); return "no-panic" }}, + {"Delete 不存在的键", func(r R) any { r.Get("SMap").Delete("nope"); return r.Get("SMap").Keys() }}, + + {"UnmarshalJSON", func(r R) any { + sub := r.Get("Sub") + err := json.Unmarshal([]byte(`{"A":"ja","B":"jb"}`), sub) + return fmt.Sprintf("%v/%v/%v", err, r.Get("Sub.A").String(), r.Get("Sub.B").String()) + }}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // 每个用例都用全新的对象, 避免写操作互相干扰 + want := call(newLegacy(newContractDoc()), c.f) + got := call(New(newContractDoc()), c.f) + if want != got { + t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got) + } + }) + } +} diff --git a/rfx_conv_test.go b/rfx_conv_test.go new file mode 100644 index 0000000..4d85b10 --- /dev/null +++ b/rfx_conv_test.go @@ -0,0 +1,158 @@ +package reflux + +import ( + "fmt" + "testing" +) + +// 标量直读快路径必须与参照实现 refx 逐格等价 —— 包括溢出截断、负数转无符号 panic +// 这些边角行为。这里对"字段类型 × 访问器方法"做全矩阵对拍。 + +type myStr string +type myInt int +type myBool bool +type myFloat float64 + +type convBox struct { + Str string + NamedS myStr + I int + NamedI myInt + I8 int8 + I16 int16 + I32 int32 + I64 int64 + U uint + U8 uint8 + U16 uint16 + U32 uint32 + U64 uint64 + F32 float32 + F64 float64 + B bool + NamedB myBool + NamedF myFloat + Neg int // 负数: 转无符号应该 panic + Over int // 300: 读成 Int8 应该截断成 44 + FracF float64 // 3.9: 读成整数应该截断成 3 + NumStr string // "123": 字符串转数字 + BadStr string // "abc": 转数字应该 panic + PtrStr *string + AnyVal any +} + +func newConvBox() *convBox { + s := "viaptr" + return &convBox{ + Str: "hello", NamedS: "named", + I: 42, NamedI: 7, + I8: -8, I16: -16, I32: -32, I64: -64, + U: 1, U8: 2, U16: 3, U32: 4, U64: 5, + F32: 1.5, F64: 2.5, + B: true, NamedB: true, NamedF: 3.5, + Neg: -1, Over: 300, FracF: 3.9, + NumStr: "123", BadStr: "abc", + PtrStr: &s, AnyVal: 99, + } +} + +// accessors 是所有会做类型转换的访问器方法 +var accessors = []struct { + name string + call func(R) any +}{ + {"String", func(r R) any { return r.String() }}, + {"Bool", func(r R) any { return r.Bool() }}, + {"Int", func(r R) any { return r.Int() }}, + {"Int8", func(r R) any { return r.Int8() }}, + {"Int16", func(r R) any { return r.Int16() }}, + {"Int32", func(r R) any { return r.Int32() }}, + {"Int64", func(r R) any { return r.Int64() }}, + {"Uint", func(r R) any { return r.Uint() }}, + {"Uint8", func(r R) any { return r.Uint8() }}, + {"Uint16", func(r R) any { return r.Uint16() }}, + {"Uint32", func(r R) any { return r.Uint32() }}, + {"Uint64", func(r R) any { return r.Uint64() }}, + {"Float32", func(r R) any { return r.Float32() }}, + {"Float64", func(r R) any { return r.Float64() }}, + {"Any", func(r R) any { return r.Any() }}, +} + +var convFields = []string{ + "Str", "NamedS", "I", "NamedI", "I8", "I16", "I32", "I64", + "U", "U8", "U16", "U32", "U64", "F32", "F64", + "B", "NamedB", "NamedF", "Neg", "Over", "FracF", + "NumStr", "BadStr", "PtrStr", "AnyVal", +} + +// callSafe 调用访问器, 把 panic 也当成一种"结果"记录下来。 +func callSafe(r R, f func(R) any) (result any, panicked bool) { + defer func() { + if rec := recover(); rec != nil { + panicked = true + } + }() + return f(r), false +} + +func TestScalarFastPathMatchesReflux(t *testing.T) { + box := newConvBox() + rx := newLegacy(box) + fx := New(box) + + for _, field := range convFields { + for _, acc := range accessors { + t.Run(field+"/"+acc.name, func(t *testing.T) { + wantVal, wantPanic := callSafe(rx.Get(field), acc.call) + gotVal, gotPanic := callSafe(fx.Get(field), acc.call) + + if wantPanic != gotPanic { + t.Fatalf("panic 行为不一致: reflux panic=%v, fastx panic=%v (值 reflux=%#v fastx=%#v)", + wantPanic, gotPanic, wantVal, gotVal) + } + if wantPanic { + return // 两边都 panic, 一致 + } + if fmt.Sprintf("%v", wantVal) != fmt.Sprintf("%v", gotVal) { + t.Fatalf("结果不一致: reflux=%#v, fastx=%#v", wantVal, gotVal) + } + }) + } + } +} + +// 单独把几个最容易出错的边角行为钉死 +func TestScalarEdgeCases(t *testing.T) { + box := newConvBox() + fx := New(box) + rx := newLegacy(box) + + // 整数溢出静默截断: 300 -> int8 = 44 + if got, want := fx.Get("Over").Int8(), rx.Get("Over").Int8(); got != want || got != 44 { + t.Fatalf("溢出截断: fastx=%d reflux=%d, 期望 44", got, want) + } + // 负数转无符号: 两边都必须 panic + if _, p1 := callSafe(fx.Get("Neg"), func(r R) any { return r.Uint8() }); !p1 { + t.Fatal("负数转 Uint8 应该 panic") + } + // 浮点截断: 3.9 -> int = 3 + if got, want := fx.Get("FracF").Int(), rx.Get("FracF").Int(); got != want || got != 3 { + t.Fatalf("浮点截断: fastx=%d reflux=%d, 期望 3", got, want) + } + // 字符串转数字仍然可用(走 cast 回退) + if got := fx.Get("NumStr").Int(); got != 123 { + t.Fatalf("字符串转数字: %d", got) + } + // 非数字字符串转数字: 两边都 panic + if _, p := callSafe(fx.Get("BadStr"), func(r R) any { return r.Int() }); !p { + t.Fatal("\"abc\" 转 Int 应该 panic") + } + // 具名类型走快路径, 结果与 reflux 一致 + if got, want := fx.Get("NamedS").String(), rx.Get("NamedS").String(); got != want { + t.Fatalf("具名 string: fastx=%q reflux=%q", got, want) + } + // 指针字段自动解引用后仍能直读 + if got := fx.Get("PtrStr").String(); got != "viaptr" { + t.Fatalf("指针字段: %q", got) + } +} diff --git a/rfx_edge_test.go b/rfx_edge_test.go new file mode 100644 index 0000000..c0530f2 --- /dev/null +++ b/rfx_edge_test.go @@ -0,0 +1,299 @@ +package reflux + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "reflect" + "sort" + "strings" + "testing" + + "git.fsdpf.net/go/reflux/valuex" +) + +// 两个之前没验证过的边角场景: +// 1. 深度嵌套的 []any 里放 R +// 2. 循环引用对象 + +// --------------------------------------------------------------------------- +// 1. []any / []R 里放 R +// --------------------------------------------------------------------------- + +// mkR 用给定构造器建出一组嵌套的 R, 供两边分别构造同构的输入。 +func mkR(newR func(any) R) (R, R) { + return newR(map[string]any{"n": "one", "deep": map[string]any{"k": "v"}}), + newR(map[string]any{"n": "two"}) +} + +func TestAnySliceContainingR(t *testing.T) { + cases := []struct { + name string + f func(newR func(any) R) any + }{ + {"New([]any{R,R}) 长度", func(n func(any) R) any { + a, b := mkR(n) + return len(n([]any{a, b}).Array()) + }}, + {"New([]any{R,R}) 取元素", func(n func(any) R) any { + a, b := mkR(n) + return n([]any{a, b}).Get("0", "n").String() + }}, + {"New([]R{R,R}) 取元素", func(n func(any) R) any { + a, b := mkR(n) + return n([]R{a, b}).Get("1", "n").String() + }}, + {"嵌套一层 []any{[]any{R}}", func(n func(any) R) any { + a, _ := mkR(n) + return n([]any{[]any{a}}).Get("0", "0", "n").String() + }}, + {"嵌套三层 + 穿透到 R 内部", func(n func(any) R) any { + a, _ := mkR(n) + return n([]any{[]any{[]any{a}}}).Get("0.0.0.deep.k").String() + }}, + {"R 装在 map 里再嵌套", func(n func(any) R) any { + a, _ := mkR(n) + return n(map[string]any{"list": []any{a}}).Get("list", "0", "n").String() + }}, + {"Array 元素再取值", func(n func(any) R) any { + a, b := mkR(n) + return n([]any{a, b}).Array()[1].Get("n").String() + }}, + {"Slice()", func(n func(any) R) any { + a, b := mkR(n) + return fmt.Sprintf("%v", n([]any{a, b}).Slice()) + }}, + {"Keys 穿透到 R", func(n func(any) R) any { + a, _ := mkR(n) + // map 的 Keys 顺序在 Go 里不确定, 排序后再比 + k := n([]any{a}).Get("0").Keys() + sort.Strings(k) + return k + }}, + {"json.Marshal([]any{R})", func(n func(any) R) any { + a, _ := mkR(n) + b, err := json.Marshal(n([]any{a})) + return fmt.Sprintf("%s / err=%v", b, err) + }}, + {"json.Marshal 嵌套两层", func(n func(any) R) any { + a, _ := mkR(n) + b, err := json.Marshal(n([]any{[]any{a}})) + return fmt.Sprintf("%s / err=%v", b, err) + }}, + {"Set 字段为 []any{R,R}", func(n func(any) R) any { + a, b := mkR(n) + d := &struct{ Items []any }{} + n(d).Set("Items", []any{a, b}) + return fmtDeref(d.Items) + }}, + {"Set 字段为 []R", func(n func(any) R) any { + a, b := mkR(n) + d := &struct{ Items []any }{} + n(d).Set("Items", []R{a, b}) + return fmtDeref(d.Items) + }}, + {"Append R 到 []any", func(n func(any) R) any { + a, _ := mkR(n) + d := &struct{ Items []any }{} + n(d).Get("Items").Append(a) + return fmtDeref(d.Items) + }}, + {"Scope 穿透 R", func(n func(any) R) any { + a, _ := mkR(n) + return n([]any{a}).Scope("0").Get("n").String() + }}, + {"Exists 穿透 R", func(n func(any) R) any { + a, _ := mkR(n) + return n([]any{a}).Exists("0", "deep", "k") + }}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + want := callGen(newLegacy, c.f) + got := callGen(New, c.f) + if want != got { + t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got) + } + }) + } +} + +// fmtDeref 按内容格式化 []any, 把指针和 Accessor 解开。 +// +// 两边存进去的都是指针, 直接 %v 比的是**地址**, 永远不会相等 —— +// 那是测试写法的问题, 不是行为差异。这里比内容。 +func fmtDeref(items []any) string { + parts := make([]string, len(items)) + for i, it := range items { + parts[i] = fmt.Sprintf("%v", derefAny(it)) + } + return "[" + strings.Join(parts, " ") + "]" +} + +func derefAny(v any) any { + if acc, ok := v.(valuex.Accessor); ok { + if raw := acc.Raw(); raw.IsValid() { + return derefReflectAny(raw) + } + return nil + } + rv := reflect.ValueOf(v) + return derefReflectAny(rv) +} + +func derefReflectAny(rv reflect.Value) any { + for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface { + if rv.IsNil() { + return nil + } + rv = rv.Elem() + } + if !rv.IsValid() { + return nil + } + return rv.Interface() +} + +// callGen 与 contract_test.go 的 call 同理, 但把构造器也参数化, +// 保证两边拿到的是**各自构造**的同构输入(而不是共用 reflux 造的 R)。 +func callGen(newR func(any) R, f func(func(any) R) any) string { + var out string + func() { + defer func() { + if rec := recover(); rec != nil { + out = "PANIC: " + fmt.Sprintf("%v", rec) + } + }() + out = fmt.Sprintf("%v", f(newR)) + }() + return out +} + +// --------------------------------------------------------------------------- +// 2. 循环引用 +// --------------------------------------------------------------------------- + +type cycNode struct { + Name string + Next *cycNode + Self any +} + +func newCycle() *cycNode { + n := &cycNode{Name: "a"} + n.Next = n + n.Self = n + return n +} + +func TestCircularReference(t *testing.T) { + cases := []struct { + name string + f func(newR func(any) R) any + }{ + {"沿指针环 Get 一圈", func(n func(any) R) any { + return n(newCycle()).Get("Next", "Next", "Next", "Name").String() + }}, + {"点号路径绕环", func(n func(any) R) any { + return n(newCycle()).Get("Next.Next.Next.Next.Name").String() + }}, + {"经 interface 字段绕环", func(n func(any) R) any { + return n(newCycle()).Get("Self", "Self", "Name").String() + }}, + {"环上 Set", func(n func(any) R) any { + c := newCycle() + n(c).Set("Next.Next.Name", "changed") + return c.Name // 环回自身, 应该改到同一个对象 + }}, + {"Exists 绕环", func(n func(any) R) any { + return n(newCycle()).Exists("Next", "Next", "Next", "Name") + }}, + {"Keys 绕环", func(n func(any) R) any { + return n(newCycle()).Get("Next", "Next").Keys() + }}, + {"json.Marshal 循环对象", func(n func(any) R) any { + b, err := json.Marshal(n(newCycle())) + return fmt.Sprintf("%s / err=%v", b, cycleErrText(err)) + }}, + {"json.Marshal 环上某一跳", func(n func(any) R) any { + b, err := json.Marshal(n(newCycle()).Get("Next")) + return fmt.Sprintf("%s / err=%v", b, cycleErrText(err)) + }}, + {"map 自引用 Get", func(n func(any) R) any { + m := map[string]any{"name": "m"} + m["self"] = m + return n(&m).Get("self", "self", "name").String() + }}, + {"map 自引用 json.Marshal", func(n func(any) R) any { + m := map[string]any{"name": "m"} + m["self"] = m + b, err := json.Marshal(n(&m)) + return fmt.Sprintf("%s / err=%v", b, cycleErrText(err)) + }}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + want := callGen(newLegacy, c.f) + got := callGen(New, c.f) + if want != got { + t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got) + } + }) + } +} + +// cycleErrText 把错误里的具体类型名抹掉 —— reflux 报的是 *reflux.rfx, +// fastx 报的是 *fastx.Value, 这属于实现细节而非行为差异。 +// 保留"是否检测到环"这个关键信息。 +func cycleErrText(err error) string { + if err == nil { + return "" + } + s := err.Error() + if strings.Contains(s, "encountered a cycle") { + return "cycle-detected" + } + return s +} + +// --------------------------------------------------------------------------- +// DeepClone 遇到循环引用会栈溢出(fatal, 不可 recover), 只能在子进程里验证。 +// 这是 reflux 既有的限制, fastx 复用 reflux.DeepClone 所以行为一致。 +// --------------------------------------------------------------------------- + +func TestDeepCloneOnCycleIsFatalForBoth(t *testing.T) { + if impl := os.Getenv("FASTX_CYCLE_IMPL"); impl != "" { + // 子进程: 真的去做深拷贝, 预期栈溢出 + n := newCycle() + if impl == "reflux" { + newLegacy(n).Scope() + } else { + New(n).Scope() + } + return + } + + for _, impl := range []string{"reflux", "fastx"} { + t.Run(impl, func(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.run=^TestDeepCloneOnCycleIsFatalForBoth$", "-test.timeout=60s") + cmd.Env = append(os.Environ(), "FASTX_CYCLE_IMPL="+impl) + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatalf("%s: 预期栈溢出, 却正常返回了", impl) + } + if !strings.Contains(string(out), "stack overflow") { + t.Fatalf("%s: 预期 stack overflow, 实际输出:\n%s", impl, truncate(string(out), 300)) + } + }) + } +} + +func truncate(s string, n int) string { + if len(s) > n { + return s[:n] + "..." + } + return s +} diff --git a/rfx_generic.go b/rfx_generic.go new file mode 100644 index 0000000..5f28e0b --- /dev/null +++ b/rfx_generic.go @@ -0,0 +1,73 @@ +package reflux + +// Get 按路径取值并直接转成目标类型 T,不产生中间的 R 包装对象。 +// +// name := reflux.Get[string](r, "Address", "City") +// age := reflux.Get[int](r, "Age") +// +// 语义与 r.Get(path...).Xxx() 严格等价,是可以放心替换的写法: +// - 路径不存在 / 未导出字段 / 下标越界 -> 返回 T 的零值 +// - 类型转换失败 -> panic,错误信息与访问器方法一致 +// +// 与链式写法的区别只在开销: 链式的 r.Get(...).String() 每次都要在堆上新建一个 +// R 包装对象,而这里直接把结果写进调用方的变量,常见标量类型下**零分配**。 +// +// 覆盖 string / int / int64 / bool / float64 五种类型;其余类型走通用路径, +// 行为与 r.Get(path...).Any().(T) 一致。 +// +// 注意: T 只出现在返回值里,无法类型推导,必须显式写出 Get[string](...)。 +func Get[T any](r R, path ...string) T { + var out T + // 用 any(&out) 而不是 any(out) 做类型分发: 前者装箱的是指针, + // 指针本身就是接口的数据字段,不需要额外堆分配;后者会把值拷进堆。 + switch p := any(&out).(type) { + case *string: + if g, ok := r.(*rfx); ok { + *p = g.getString(path...) + } else { + *p = r.Get(detachPath(path)...).String() + } + case *int: + if g, ok := r.(*rfx); ok { + *p = g.getInt(path...) + } else { + *p = r.Get(detachPath(path)...).Int() + } + case *int64: + if g, ok := r.(*rfx); ok { + *p = g.getInt64(path...) + } else { + *p = r.Get(detachPath(path)...).Int64() + } + case *bool: + if g, ok := r.(*rfx); ok { + *p = g.getBool(path...) + } else { + *p = r.Get(detachPath(path)...).Bool() + } + case *float64: + if g, ok := r.(*rfx); ok { + *p = g.getFloat64(path...) + } else { + *p = r.Get(detachPath(path)...).Float64() + } + default: + // 非标量类型: 退回通用路径 + if x, ok := r.Get(detachPath(path)...).Any().(T); ok { + out = x + } + } + return out +} + +// detachPath 复制一份路径切片。 +// +// 这一步看着多余,实则必要: 下面那些 r.Get(...) 是**接口动态调用**,逃逸分析 +// 看不穿,于是判定 path 整体逃逸 —— 哪怕运行时走的是上面的快分支。 +// 在慢分支里复制一份切断数据流,快分支的可变参数才能留在栈上。 +// 少了这一步,Get[string] 会从 0 分配退化成 1 次 48 字节分配。 +func detachPath(p []string) []string { + q := make([]string, len(p)) + copy(q, p) + return q +} diff --git a/rfx_generic_test.go b/rfx_generic_test.go new file mode 100644 index 0000000..b8e37b1 --- /dev/null +++ b/rfx_generic_test.go @@ -0,0 +1,162 @@ +package reflux + +// 泛型 Get[T] 的等价性与分配情况。 +// +// 分配这块用 testing.AllocsPerRun 精确统计, 并且必须写进包级 sink 变量 —— +// 丢弃结果的写法会被编译器优化掉, 测出假的 0 分配。 + +import ( + "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) + } + } +} diff --git a/rfx_reflect.go b/rfx_reflect.go new file mode 100644 index 0000000..2a9763b --- /dev/null +++ b/rfx_reflect.go @@ -0,0 +1,1009 @@ +package reflux + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + + "git.fsdpf.net/go/reflux/valuex" + "github.com/spf13/cast" +) + +// refx 是基于 reflect.Value 的实现, 现在作为**冷路径回退**存在。 +// +// 热路径(Get/Set/标量访问器)由 rfx 用类型布局缓存 + unsafe 指针运算完成; +// 语义复杂、调用频次低的操作 —— 复合类型赋值、Append、Delete、 +// 容器转换(StringMap*/Slice*)、map 的 Keys —— 仍然走这里。 +// +// 这样划分的理由: 这些操作的语义(指针字段是"设置指向的值"、[]any 逐元素转换、 +// map 填充进 struct ...)非常琐碎, 重新实现一遍必然引入行为偏差, +// 而它们本来就不在性能热点上。 +type refx struct { + // value 存储底层的反射值 + // 该值应该是指针类型,以支持修改操作 + value reflect.Value +} + +// Get 通过路径获取嵌套字段的值 +func (r *refx) Get(p ...string) R { + return &refx{value: getValueByPath(r.value, p...)} +} + +// Scope 创建一个指定路径的作用域视图(深度克隆) +func (r *refx) Scope(p ...string) R { + v := getValueByPath(r.value, p...) + if !v.IsValid() { + return &refx{value: reflect.Value{}} + } + + // 深度克隆值 + cloned := DeepClone(v) + return &refx{value: cloned} +} + +// Set 设置指定路径的值,支持链式调用 +// 如果路径不存在或设置失败,会 panic 并提供详细错误信息 +func (r *refx) Set(key string, v any) R { + // 展开路径 + keys := expandPath(key) + if len(keys) == 0 { + panic(ErrEmptyPath) + } + + // 如果只有一个键,直接设置 + if len(keys) == 1 { + target := r.getParentValue() + if !target.IsValid() { + panic(NewErrInvalidValueForPath(key)) + } + if !target.CanSet() { + panic(NewErrCannotSetPath(key)) + } + if err := r.setFieldValue(target, keys[0], v); err != nil { + panic(NewErrSetFailed(key, err)) + } + return r + } + + // 多个键的情况,需要特殊处理 map 中的 struct + if err := r.setNestedValue(r.value, keys, v); err != nil { + panic(NewErrSetFailed(key, err)) + } + return r +} + +// Append 追加指定路径的值 +// 参数 items 为要追加的值 +// 返回当前 R 实例以支持链式调用 +func (r *refx) Append(items ...any) R { + // 没有要追加的元素,直接返回 + if len(items) == 0 { + return r + } + + target := r.getParentValue() + target = deref(target) + + if !target.IsValid() { + panic(ErrAppendNilValue) + } + if target.Kind() != reflect.Slice { + panic(ErrAppendNotSupported) + } + + // 一次性构造所有要追加的元素,然后调用一次 reflect.Append + elemType := target.Type().Elem() + newValues := make([]reflect.Value, len(items)) + for i, item := range items { + newElem := reflect.New(elemType).Elem() + if err := r.setValue(newElem, item); err != nil { + panic(NewErrAppendItemFailed(i, err)) + } + newValues[i] = newElem + } + + target.Set(reflect.Append(target, newValues...)) + return r +} + +// setNestedValue 递归设置嵌套值,特殊处理 map 中的 struct +func (r *refx) setNestedValue(current reflect.Value, keys []string, v any) error { + // 解引用指针和接口 + current = derefWithAccessor(current) + if !current.IsValid() { + return ErrNilPointerInPath + } + + // 如果只剩一个键,直接设置 + if len(keys) == 1 { + if !current.CanSet() { + return ErrValueCannotBeSet + } + return r.setFieldValue(current, keys[0], v) + } + + // 多个键的情况 + firstKey := keys[0] + remainingKeys := keys[1:] + + switch current.Kind() { + case reflect.Struct: + field := tryStructFieldValue(current, firstKey) + if !field.IsValid() { + return NewErrFieldNotFound(firstKey) + } + return r.setNestedValue(field, remainingKeys, v) + + case reflect.Map: + // Map 的特殊处理 + // 使用 tryMapFieldKey 获取实际的键 + actualKey := tryMapFieldKey(current, firstKey) + if !actualKey.IsValid() { + return NewErrKeyNotFound(firstKey) + } + + mapValue := current.MapIndex(actualKey) + if !mapValue.IsValid() { + return NewErrValueNotFound(firstKey) + } + + // 解开 interface 包装获取实际的值 + actualValue := mapValue + for actualValue.Kind() == reflect.Interface && !actualValue.IsNil() { + actualValue = actualValue.Elem() + } + + // 创建 map 值的副本以便修改 + valueCopy := reflect.New(actualValue.Type()).Elem() + valueCopy.Set(actualValue) + + // 在副本上递归设置值 + if err := r.setNestedValue(valueCopy, remainingKeys, v); err != nil { + return err + } + + // 将修改后的值设置回 map,使用实际找到的键 + current.SetMapIndex(actualKey, valueCopy) + return nil + + case reflect.Slice, reflect.Array: + idx, err := strconv.Atoi(firstKey) + if err != nil || idx < 0 || idx >= current.Len() { + return NewErrInvalidIndex(firstKey, current.Len()) + } + elem := current.Index(idx) + if !elem.IsValid() { + return NewErrElementInvalid(firstKey) + } + return r.setNestedValue(elem, remainingKeys, v) + } + + return NewErrUnsupportedType(current.Kind(), firstKey) +} + +// getParentValue 获取父级值的辅助方法 +func (r *refx) getParentValue(p ...string) reflect.Value { + if len(p) == 0 { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + return v + } + return getValueByPath(r.value, p...) +} + +// setFieldValue 设置字段值的辅助方法 +// 返回 error 包含详细的设置失败信息 +func (r *refx) setFieldValue(target reflect.Value, key string, v any) error { + target = deref(target) + if !target.IsValid() { + return ErrTargetNilPointer + } + + switch target.Kind() { + case reflect.Struct: + field := tryStructFieldValue(target, key) + if !field.IsValid() { + return NewErrFieldNotFoundInStruct(key, target.Type()) + } + if !field.CanSet() { + return NewErrFieldCannotSet(key, field.Type()) + } + return r.setValue(field, v) + case reflect.Map: + if target.IsNil() { + target.Set(reflect.MakeMap(target.Type())) + } + + // 先尝试使用 tryMapFieldKey 检查字段是否已存在并获取实际的键 + actualKey := tryMapFieldKey(target, key) + if actualKey.IsValid() { + // 字段已存在,创建新值用于设置 + newValue := reflect.New(target.Type().Elem()).Elem() + if err := r.setValue(newValue, v); err != nil { + return err + } + target.SetMapIndex(actualKey, newValue) + return nil + } + + // 字段不存在,创建新的 map 值 + // 处理 nil 值的情况 + if v == nil { + target.SetMapIndex(reflect.ValueOf(key), reflect.Zero(target.Type().Elem())) + return nil + } + + val := reflect.ValueOf(v) + if !val.Type().AssignableTo(target.Type().Elem()) { + // 尝试转换 + if val.Type().ConvertibleTo(target.Type().Elem()) { + val = val.Convert(target.Type().Elem()) + } else { + return NewErrCannotAssign(val.Type(), target.Type().Elem()) + } + } + target.SetMapIndex(reflect.ValueOf(key), val) + return nil + case reflect.Slice: + idx, err := strconv.Atoi(key) + if err != nil { + return NewErrInvalidSliceIndex(key, err) + } + + // 对于切片,支持使用索引 -1 追加新元素(插入到切片前面) + if idx == -1 { + elemType := target.Type().Elem() + newElem := reflect.New(elemType).Elem() + if err := r.setValue(newElem, v); err != nil { + return err + } + // 将新元素放在前面,原有元素顺序后移 + newSlice := reflect.MakeSlice(target.Type(), 0, target.Len()+1) + newSlice = reflect.Append(newSlice, newElem) + newSlice = reflect.AppendSlice(newSlice, target) + target.Set(newSlice) + return nil + } + + if idx < 0 || idx >= target.Len() { + return NewErrSliceIndexOutOfRange(idx, target.Len()) + } + elem := target.Index(idx) + if !elem.CanSet() { + return NewErrSliceElementCannotSet(idx) + } + return r.setValue(elem, v) + case reflect.Array: + idx, err := strconv.Atoi(key) + if err != nil { + return NewErrInvalidArrayIndex(key, err) + } + if idx < 0 || idx >= target.Len() { + return NewErrArrayIndexOutOfRange(idx, target.Len()) + } + elem := target.Index(idx) + if !elem.CanSet() { + return NewErrArrayElementCannotSet(idx) + } + return r.setValue(elem, v) + } + return NewErrUnsupportedTargetType(target.Kind()) +} + +// setValue 设置值的辅助方法 +// 使用 cast 库进行智能类型转换,支持更多的转换场景 +// 返回 error 包含详细的类型转换失败信息 +func (r *refx) setValue(field reflect.Value, v any) error { + val, _, err := normalizeInputValue(v) + + if err != nil { + return NewErrNormalizeInputFailed(err) + } + + targetType := field.Type() + + if !val.IsValid() { + val = reflect.Zero(targetType) + } + + // 如果 val 有效,进行正常的类型处理 + // 统一解开最外层的 interface 包装,便于后续根据底层实际类型做处理 + for val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + + // 尝试直接赋值(类型完全匹配) + if val.Type().AssignableTo(targetType) { + field.Set(val) + return nil + } + + // 如果源值是指针但目标不是指针,尝试解引用后再赋值 + if val.Kind() == reflect.Ptr && !val.IsNil() && targetType.Kind() != reflect.Ptr { + derefVal := val.Elem() + if derefVal.Type().AssignableTo(targetType) { + field.Set(derefVal) + return nil + } + // 解引用后继续使用下面的逻辑处理 + val = derefVal + } + + switch targetType.Kind() { + case reflect.Ptr: + // 处理指针类型 + // 如果传入的值已经是指针类型,尝试直接赋值 + if val.Type().AssignableTo(targetType) { + field.Set(val) + return nil + } + + // 如果传入的值不是指针,创建新指针并设置值 + elemType := targetType.Elem() + newPtr := reflect.New(elemType) + + // 递归设置指针指向的值 + if err := r.setValue(newPtr.Elem(), v); err != nil { + return err + } + + field.Set(newPtr) + return nil + + case reflect.Slice: // 处理切片类型,支持从通用切片(如 []any)转换 + if val.Kind() != reflect.Slice && val.Kind() != reflect.Array { + return NewErrCannotConvertToSlice(val.Type(), targetType) + } + + newSlice := reflect.MakeSlice(targetType, val.Len(), val.Len()) + + for i := 0; i < val.Len(); i++ { + if err := r.setValue(newSlice.Index(i), val.Index(i).Interface()); err != nil { + return NewErrSetSliceElementFailed(i, err) + } + } + + field.Set(newSlice) + + return nil + + case reflect.Struct: + // 遍历目标结构体的字段,从源值(结构体或 map)中按字段名取值并设置 + // 仅支持从 struct 或 map 填充 + if val.Kind() != reflect.Struct && val.Kind() != reflect.Map { + return NewErrCannotConvertToStruct(val.Type(), targetType) + } + + fieldType := field.Type() + for i := 0; i < fieldType.NumField(); i++ { + dstField := field.Field(i) + if !dstField.CanSet() { + continue + } + + valField := getValueByPath(val, fieldType.Field(i).Name) + + if !valField.IsValid() { + continue + } + + // 使用 setValue 复用现有的类型转换逻辑,忽略单个字段失败 + if err := r.setValue(dstField, valField.Interface()); err != nil { + return NewErrSetStructFieldFailed(fieldType.Field(i).Name, err) + } + } + return nil + + } + + // 优先使用 cast 进行智能类型转换 + // 这样可以处理 string <-> number, number <-> bool 等常见转换 + var converted any + + switch targetType.Kind() { + case reflect.Bool: + converted, err = cast.ToBoolE(v) + case reflect.Int: + converted, err = cast.ToIntE(v) + case reflect.Int8: + converted, err = cast.ToInt8E(v) + case reflect.Int16: + converted, err = cast.ToInt16E(v) + case reflect.Int32: + converted, err = cast.ToInt32E(v) + case reflect.Int64: + converted, err = cast.ToInt64E(v) + case reflect.Uint: + converted, err = cast.ToUintE(v) + case reflect.Uint8: + converted, err = cast.ToUint8E(v) + case reflect.Uint16: + converted, err = cast.ToUint16E(v) + case reflect.Uint32: + converted, err = cast.ToUint32E(v) + case reflect.Uint64: + converted, err = cast.ToUint64E(v) + case reflect.Float32: + converted, err = cast.ToFloat32E(v) + case reflect.Float64: + converted, err = cast.ToFloat64E(v) + case reflect.String: + converted, err = cast.ToStringE(v) + default: + return NewErrUnsupportedTargetTypeForValue(targetType, fmt.Sprintf("%T", v)) + } + + if err != nil { + // 如果 cast 失败,尝试标准的反射类型转换作为后备 + if val.IsValid() && val.Type().ConvertibleTo(field.Type()) { + field.Set(val.Convert(field.Type())) + return nil + } + return NewErrCannotConvertValue(fmt.Sprintf("%T", v), targetType, err) + } + + field.Set(reflect.ValueOf(converted)) + + return nil +} + +// Delete 删除指定路径的值,支持链式调用 +// 如果删除失败会 panic +func (r *refx) Delete(p ...string) R { + if len(p) == 0 { + panic(ErrEmptyPath) + } + + // 展开路径 + keys := expandPath(p...) + if len(keys) == 0 { + panic("rfx: empty path") + } + + target := r.getParentValue(keys[:len(keys)-1]...) + if !target.IsValid() { + panic(fmt.Sprintf("rfx: invalid path '%s'", strings.Join(keys[:len(keys)-1], "."))) + } + + lastKey := keys[len(keys)-1] + for target.Kind() == reflect.Ptr || target.Kind() == reflect.Interface { + if target.IsNil() { + panic(fmt.Sprintf("rfx: nil value at path '%s'", strings.Join(keys[:len(keys)-1], "."))) + } + target = target.Elem() + } + + switch target.Kind() { + case reflect.Map: + target.SetMapIndex(reflect.ValueOf(lastKey), reflect.Value{}) + return r + case reflect.Slice: + idx, err := strconv.Atoi(lastKey) + if err != nil { + panic(fmt.Sprintf("rfx: invalid slice index '%s'", lastKey)) + } + if idx < 0 || idx >= target.Len() { + panic(fmt.Sprintf("rfx: slice index out of range: %d", idx)) + } + // 删除切片元素 + newSlice := reflect.AppendSlice( + target.Slice(0, idx), + target.Slice(idx+1, target.Len()), + ) + target.Set(newSlice) + return r + default: + panic(fmt.Sprintf("rfx: cannot delete from type %s", target.Kind())) + } +} + +// Exists 检查指定路径的值是否存在 +func (r *refx) Exists(p ...string) bool { + return getValueByPath(r.value, p...).IsValid() +} + +// Array 将当前值转换为 R 切片 +func (r *refx) Array() []R { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return nil + } + + result := make([]R, v.Len()) + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + if elem.Kind() == reflect.Interface && !elem.IsNil() { + if r, ok := elem.Interface().(R); ok { + result[i] = r + continue + } + } + result[i] = &refx{value: elem} + } + return result +} + +// keys 返回当前映射或结构体的所有键名 +func (r *refx) Keys() []string { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + switch v.Kind() { + case reflect.Map: + keys := v.MapKeys() + result := make([]string, len(keys)) + for i, k := range keys { + result[i] = fmt.Sprint(k.Interface()) + } + return result + case reflect.Struct: + t := v.Type() + result := make([]string, t.NumField()) + for i := 0; i < t.NumField(); i++ { + result[i] = t.Field(i).Name + } + return result + } + return nil +} + +// Raw 返回底层的 reflect.Value +func (r *refx) Raw() reflect.Value { + return r.value +} + +// Lookup 根据路径查找并返回对应值的访问器 +func (r *refx) Lookup(path string) (valuex.Accessor, bool) { + v := r.Get(path) + if v.Exists() { + return v, true + } + return valuex.Nil, false +} + +// MustLookup 根据路径查找并直接返回对应值的访问器 +// 如果路径不存在,返回 valuex.Nil 访问器(所有方法返回零值) +func (r *refx) MustLookup(path string) valuex.Accessor { + v := r.Get(path) + if v.Exists() { + return v + } + return valuex.Nil +} + +// Ptr 返回指向当前值的指针 +func (r *refx) Ptr() any { + v := r.value + if v.Kind() == reflect.Ptr { + return v.Interface() + } + if v.CanAddr() { + return v.Addr().Interface() + } + return nil +} + +// Any 将当前值转换为 any 类型 +func (r *refx) Any() any { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + if !v.IsValid() { + return nil + } + return v.Interface() +} + +// Bool 将当前值转换为 bool 类型 +func (r *refx) Bool() bool { + result, err := cast.ToBoolE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to bool: %v", err)) + } + return result +} + +// Float64 将当前值转换为 float64 类型 +func (r *refx) Float64() float64 { + result, err := cast.ToFloat64E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to float64: %v", err)) + } + return result +} + +// Float32 将当前值转换为 float32 类型 +func (r *refx) Float32() float32 { + result, err := cast.ToFloat32E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to float32: %v", err)) + } + return result +} + +// Int64 将当前值转换为 int64 类型 +func (r *refx) Int64() int64 { + result, err := cast.ToInt64E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int64: %v", err)) + } + return result +} + +// Int32 将当前值转换为 int32 类型 +func (r *refx) Int32() int32 { + result, err := cast.ToInt32E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int32: %v", err)) + } + return result +} + +// Int16 将当前值转换为 int16 类型 +func (r *refx) Int16() int16 { + result, err := cast.ToInt16E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int16: %v", err)) + } + return result +} + +// Int8 将当前值转换为 int8 类型 +func (r *refx) Int8() int8 { + result, err := cast.ToInt8E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int8: %v", err)) + } + return result +} + +// Int 将当前值转换为 int 类型 +func (r *refx) Int() int { + result, err := cast.ToIntE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to int: %v", err)) + } + return result +} + +// Uint 将当前值转换为 uint 类型 +func (r *refx) Uint() uint { + result, err := cast.ToUintE(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint: %v", err)) + } + return result +} + +// Uint64 将当前值转换为 uint64 类型 +func (r *refx) Uint64() uint64 { + result, err := cast.ToUint64E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint64: %v", err)) + } + return result +} + +// Uint32 将当前值转换为 uint32 类型 +func (r *refx) Uint32() uint32 { + result, err := cast.ToUint32E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint32: %v", err)) + } + return result +} + +// Uint16 将当前值转换为 uint16 类型 +func (r *refx) Uint16() uint16 { + result, err := cast.ToUint16E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint16: %v", err)) + } + return result +} + +// Uint8 将当前值转换为 uint8 类型 +func (r *refx) Uint8() uint8 { + result, err := cast.ToUint8E(r.Any()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to uint8: %v", err)) + } + return result +} + +// String 将当前值转换为 string 类型 +func (r *refx) String() string { + data := r.Any() + result, err := cast.ToStringE(data) + if err != nil { + if b, e := json.Marshal(data); e == nil { + result = string(b) + } else { + panic(fmt.Sprintf("rfx: failed to convert to string: %v", err)) + } + } + return result +} + +// StringMapString 将当前值转换为 map[string]string 类型 +func (r *refx) StringMapString() map[string]string { + data := r.Any() + + // 首先尝试使用 cast 进行转换 + result, err := cast.ToStringMapStringE(data) + if err == nil { + return result + } + + // cast 失败后,尝试自定义转换逻辑 + defer func() { + if p := recover(); p != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]string, cast error: %v, custom conversion error: %v", err, p)) + } + }() + + result = make(map[string]string) + for _, k := range r.Keys() { + result[k] = r.Get(k).String() + } + + return result +} + +// StringMapStringSlice 将当前值转换为 map[string][]string 类型 +func (r *refx) StringMapStringSlice() map[string][]string { + data := r.Any() + + // 首先尝试使用 cast 进行转换 + result, err := cast.ToStringMapStringSliceE(data) + if err == nil { + return result + } + + // cast 失败后,尝试自定义转换逻辑 + defer func() { + if p := recover(); p != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string][]string, cast error: %v, custom conversion error: %v", err, p)) + } + }() + + result = make(map[string][]string) + for _, k := range r.Keys() { + result[k] = r.Get(k).StringSlice() + } + + return result +} + +// StringMapBool 将当前值转换为 map[string]bool 类型 +func (r *refx) StringMapBool() map[string]bool { + data := r.Any() + + // 首先尝试使用 cast 进行转换 + result, err := cast.ToStringMapBoolE(data) + if err == nil { + return result + } + + // cast 失败后,尝试自定义转换逻辑 + defer func() { + if p := recover(); p != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]bool, cast error: %v, custom conversion error: %v", err, p)) + } + }() + + result = make(map[string]bool) + for _, k := range r.Keys() { + result[k] = r.Get(k).Bool() + } + + return result +} + +// StringMapInt 将当前值转换为 map[string]int 类型 +func (r *refx) StringMapInt() map[string]int { + data := r.Any() + + // 首先尝试使用 cast 进行转换 + result, err := cast.ToStringMapIntE(data) + if err == nil { + return result + } + + // cast 失败后,尝试自定义转换逻辑 + defer func() { + if p := recover(); p != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]int, cast error: %v, custom conversion error: %v", err, p)) + } + }() + + result = make(map[string]int) + for _, k := range r.Keys() { + result[k] = r.Get(k).Int() + } + + return result +} + +// StringMapInt64 将当前值转换为 map[string]int64 类型 +func (r *refx) StringMapInt64() map[string]int64 { + data := r.Any() + + // 首先尝试使用 cast 进行转换 + result, err := cast.ToStringMapInt64E(data) + if err == nil { + return result + } + + // cast 失败后,尝试自定义转换逻辑 + defer func() { + if p := recover(); p != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]int64, cast error: %v, custom conversion error: %v", err, p)) + } + }() + + result = make(map[string]int64) + for _, k := range r.Keys() { + result[k] = r.Get(k).Int64() + } + + return result +} + +// StringMap 将当前值转换为 map[string]any 类型 +func (r *refx) StringMap() map[string]any { + data := r.Any() + + // 首先尝试使用 cast 进行转换 + result, err := cast.ToStringMapE(data) + if err == nil { + return result + } + + // cast 失败后,尝试自定义转换逻辑 + defer func() { + if p := recover(); p != nil { + panic(fmt.Sprintf("rfx: failed to convert to map[string]any, cast error: %v, custom conversion error: %v", err, p)) + } + }() + + result = make(map[string]any) + for _, k := range r.Keys() { + result[k] = r.Get(k).Any() + } + + return result +} + +// Slice 将当前值转换为 []any 切片 +func (r *refx) Slice() (result []any) { + v := r.value + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + if !v.IsValid() { + return nil + } else if v.Kind() == reflect.String { + if str := r.String(); len(str) >= 2 && str[0] == '[' { + if err := json.Unmarshal([]byte(str), &result); err == nil { + return result + } + } + } else if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return nil + } + + result = make([]any, v.Len()) + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + for elem.Kind() == reflect.Ptr || elem.Kind() == reflect.Interface { + if elem.IsNil() { + break + } + elem = elem.Elem() + } + if elem.IsValid() { + result[i] = elem.Interface() + } + } + return result +} + +// BoolSlice 将当前值转换为 []bool 切片 +func (r *refx) BoolSlice() []bool { + result, err := cast.ToBoolSliceE(r.Slice()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to []bool: %v", err)) + } + return result +} + +// StringSlice 将当前值转换为 []string 切片 +func (r *refx) StringSlice() []string { + result, err := cast.ToStringSliceE(r.Slice()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to []string: %v", err)) + } + return result +} + +// IntSlice 将当前值转换为 []int 切片 +func (r *refx) IntSlice() []int { + result, err := cast.ToIntSliceE(r.Slice()) + if err != nil { + panic(fmt.Sprintf("rfx: failed to convert to []int: %v", err)) + } + return result +} + +// MarshalJSON 实现 json.Marshaler 接口 +// 将当前值序列化为 JSON 字节数组 +func (r *refx) MarshalJSON() ([]byte, error) { + return json.Marshal(r.Any()) +} + +// UnmarshalJSON 实现 json.Unmarshaler 接口 +// 从 JSON 字节数组反序列化到当前值 +func (r *refx) UnmarshalJSON(data []byte) error { + // 先解析到 any 类型 + var v any + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + // 获取当前值的实际类型 + target := r.value + for target.Kind() == reflect.Ptr || target.Kind() == reflect.Interface { + if target.IsNil() { + break + } + target = target.Elem() + } + + // 如果当前值无效或为 nil,创建一个新的 map[string]any + if !target.IsValid() || !target.CanSet() { + r.value = reflect.ValueOf(&v).Elem() + return nil + } + + // 尝试将解析的值设置到当前值 + newValue := reflect.ValueOf(v) + if newValue.Type().AssignableTo(target.Type()) { + target.Set(newValue) + return nil + } + + // 如果类型不匹配,尝试使用 setValue 进行转换 + if err := r.setValue(target, v); err != nil { + return NewErrUnmarshalFailed(target.Type(), err) + } + + return nil +} diff --git a/rfx_typedesc.go b/rfx_typedesc.go new file mode 100644 index 0000000..041c25b --- /dev/null +++ b/rfx_typedesc.go @@ -0,0 +1,215 @@ +// 类型布局描述符: 把 struct 的字段偏移量、元素类型大小等信息构建一次并全局缓存, +// 热路径上不再走 reflect 的按名字段查找。 +package reflux + +import ( + "reflect" + "sync" +) + +// rfxFieldDesc 描述一个字段在其所属 struct 里的位置和类型。 +type rfxFieldDesc struct { + // Name 字段名(原始声明名) + Name string + // Offset 字段相对于 struct 起始地址的字节偏移量 + Offset uintptr + // Type 字段类型的描述符, 递归引用以支持嵌套 struct + Type *rfxTypeDesc + // Exported 字段是否是导出字段。 + // + // 这是本原型最关键的安全标记: reflect.NewAt 构造出来的 Value 是可寻址且 + // **可写**的, 它不带 reflect 正常路径上的 flagRO 只读标记, 也就是说 Go 语言 + // 层面"不能修改未导出字段"这条保护在 unsafe 路径上完全失效。因此必须由我们 + // 自己在每一次 Get/Set 前显式拒绝未导出字段。 + Exported bool + // index 在 reflect.Type 里的字段下标, 回退到 reflect 路径时使用 + index int +} + +// rfxTypeDesc 描述一个类型的内存布局, 构建一次之后全局复用。 +type rfxTypeDesc struct { + rtype reflect.Type + Kind reflect.Kind + Size uintptr + + // fields 按声明顺序保存所有字段, 用于线性扫描查找 + fields []rfxFieldDesc + // fieldMap 按字段名索引, 用于 map 查找。 + // 除了原始字段名, 还额外收录了"首字母小写"的别名, 以兼容 reflux 的 + // tryStructFieldValue 行为(Get("address") 能命中 Address 字段)。 + fieldMap map[string]*rfxFieldDesc + + // Elem 是 ptr/slice/array 的元素类型描述符 + Elem *rfxTypeDesc + // elemSize 是元素类型的字节大小, slice/array 下标寻址用 + elemSize uintptr + // arrayLen 是数组长度(仅 Kind == Array 时有效) + arrayLen int + + // ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次 + ptrElemKind reflect.Kind + + // fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。 + // 热路径上只要比较一个字节, 就能决定走不走零分配快路径。 + fast fastKind +} + +// fastKind 标记"类型与 Go 内置标量完全一致"的情况。 +// 注意必须是**完全一致**: type MyStr string 的 Kind 也是 String, 但直接按 +// string 写入会绕过 reflux 原有的 cast 转换语义, 所以这里只认原生类型。 +type fastKind uint8 + +const ( + fkNone fastKind = iota + fkString + fkInt + fkBool + fkFloat64 +) + +var ( + typeString = reflect.TypeOf("") + typeInt = reflect.TypeOf(int(0)) + typeBool = reflect.TypeOf(false) + typeFloat64 = reflect.TypeOf(float64(0)) +) + +func fastKindOf(t reflect.Type) fastKind { + switch t { + case typeString: + return fkString + case typeInt: + return fkInt + case typeBool: + return fkBool + case typeFloat64: + return fkFloat64 + } + return fkNone +} + +// isScalar 报告该类型是不是可以用 cast 直接转换的标量。 +// 只有标量目标才走 fastx 自己的赋值逻辑; 复合类型(指针/切片/结构体/map/interface) +// 的赋值语义由 reflux.setValue 定义, 重新实现必然产生偏差, 一律交回它处理。 +func (td *rfxTypeDesc) isScalar() bool { + switch td.Kind { + case reflect.String, reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Uintptr, reflect.Float32, reflect.Float64: + return true + } + return false +} + +// lookupField 用 map 查找字段, 未找到返回 nil。 +func (td *rfxTypeDesc) lookupField(name string) *rfxFieldDesc { + return td.fieldMap[name] +} + +var ( + // descCache 是全局类型描述符缓存。reflect.Type 本身可比较, 直接做 key。 + descCache sync.Map // map[reflect.Type]*rfxTypeDesc + // descMu 只在缓存未命中(冷路径)时加锁, 保证同一时刻只有一个构建者, + // 并且保证带环的类型图整体构建完成后才发布, 读者永远看不到半成品。 + descMu sync.Mutex +) + +// rfxDescriptorOf 返回类型 t 的布局描述符, 第一次遇到时构建, 之后命中缓存。 +func rfxDescriptorOf(t reflect.Type) *rfxTypeDesc { + if v, ok := descCache.Load(t); ok { + return v.(*rfxTypeDesc) + } + + descMu.Lock() + defer descMu.Unlock() + + // 双重检查: 可能在抢锁期间已经被别的 goroutine 构建好了 + if v, ok := descCache.Load(t); ok { + return v.(*rfxTypeDesc) + } + + // pending 保存本次构建过程中新建的所有描述符。自引用类型 + // (type Node struct{ Next *Node }) 靠它终止递归。 + pending := make(map[reflect.Type]*rfxTypeDesc, 8) + td := buildLocked(t, pending) + + // 整张类型图都填充完毕之后再统一发布。sync.Map.Store 提供的 + // happens-before 保证了读者 Load 到任何一个描述符时, 它引用到的 + // 其它描述符也已经写完。 + for k, v := range pending { + descCache.Store(k, v) + } + return td +} + +// buildLocked 在持有 descMu 的前提下递归构建描述符。 +func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxTypeDesc { + if v, ok := descCache.Load(t); ok { + return v.(*rfxTypeDesc) + } + if td, ok := pending[t]; ok { + return td + } + + td := &rfxTypeDesc{ + rtype: t, + Kind: t.Kind(), + Size: t.Size(), + fast: fastKindOf(t), + } + // 先登记再递归, 否则自引用类型会栈溢出 + pending[t] = td + + switch t.Kind() { + case reflect.Struct: + n := t.NumField() + td.fields = make([]rfxFieldDesc, n) + td.fieldMap = make(map[string]*rfxFieldDesc, n*2) + for i := 0; i < n; i++ { + sf := t.Field(i) + td.fields[i] = rfxFieldDesc{ + Name: sf.Name, + Offset: sf.Offset, + Type: buildLocked(sf.Type, pending), + Exported: sf.IsExported(), + index: i, + } + } + for i := range td.fields { + f := &td.fields[i] + td.fieldMap[f.Name] = f + } + // 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。 + // 只在不与真实字段名冲突时才登记。 + for i := range td.fields { + f := &td.fields[i] + if len(f.Name) > 0 && f.Name[0] >= 'A' && f.Name[0] <= 'Z' { + alias := string(f.Name[0]+32) + f.Name[1:] + if _, exists := td.fieldMap[alias]; !exists { + td.fieldMap[alias] = f + } + } + } + + case reflect.Ptr: + td.Elem = buildLocked(t.Elem(), pending) + td.ptrElemKind = t.Elem().Kind() + + case reflect.Slice: + td.Elem = buildLocked(t.Elem(), pending) + td.elemSize = t.Elem().Size() + + case reflect.Array: + td.Elem = buildLocked(t.Elem(), pending) + td.elemSize = t.Elem().Size() + td.arrayLen = t.Len() + + case reflect.Map: + // map 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型, + // 实际取值走 reflect 回退路径。 + td.Elem = buildLocked(t.Elem(), pending) + } + + return td +} diff --git a/rfx_unsafe_test.go b/rfx_unsafe_test.go new file mode 100644 index 0000000..d1445f0 --- /dev/null +++ b/rfx_unsafe_test.go @@ -0,0 +1,516 @@ +package reflux + +// 新实现(rfx)特有的行为验证: 未导出字段不可达、map 元素副本语义、 +// 地址别名、自引用类型的描述符构建、并发安全。 + +import ( + "encoding/json" + "reflect" + "strings" + "sync" + "testing" +) + +type uxInner struct { + City string + Zip int + Active bool + Ratio float64 +} + +type uxMiddle struct { + Inner uxInner + InnerPtr *uxInner + Any any +} + +type uxOuter struct { + Name string + Mid uxMiddle + Tags []string + Nums [3]int + Meta map[string]string + Objs map[string]uxInner + secret string // 未导出字段, 必须无法读写 + Structs []uxInner +} + +func newUxOuter() *uxOuter { + return &uxOuter{ + Name: "root", + Mid: uxMiddle{ + Inner: uxInner{City: "Beijing", Zip: 100000, Active: true, Ratio: 1.5}, + InnerPtr: &uxInner{City: "Shanghai", Zip: 200000}, + Any: uxInner{City: "Shenzhen", Zip: 518000}, + }, + Tags: []string{"a", "b", "c"}, + Nums: [3]int{10, 20, 30}, + Meta: map[string]string{"k1": "v1", "k2": "v2"}, + Objs: map[string]uxInner{"o1": {City: "Chengdu", Zip: 610000}}, + secret: "should-not-be-reachable", + Structs: []uxInner{{City: "Wuhan"}, {City: "Xian"}}, + } +} + +// --------------------------------------------------------------------------- +// Get: 各种路径形态 +// --------------------------------------------------------------------------- + +func TestGetPaths(t *testing.T) { + o := newUxOuter() + r := New(o) + + cases := []struct { + name string + path []string + want string + }{ + {"单层字段", []string{"Name"}, "root"}, + {"多层嵌套", []string{"Mid", "Inner", "City"}, "Beijing"}, + {"点号路径", []string{"Mid.Inner.City"}, "Beijing"}, + {"点号混合", []string{"Mid.Inner", "City"}, "Beijing"}, + {"指针自动解引用", []string{"Mid", "InnerPtr", "City"}, "Shanghai"}, + {"interface 自动解引用", []string{"Mid", "Any", "City"}, "Shenzhen"}, + {"slice 下标", []string{"Tags", "1"}, "b"}, + {"array 下标", []string{"Nums", "2"}, "30"}, + {"map 键", []string{"Meta", "k1"}, "v1"}, + {"map 里的 struct", []string{"Objs", "o1", "City"}, "Chengdu"}, + {"slice 里的 struct", []string{"Structs", "1", "City"}, "Xian"}, + {"首字母小写兼容", []string{"mid", "inner", "city"}, "Beijing"}, + {"前后多余点号", []string{".Mid..Inner.City."}, "Beijing"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := r.Get(c.path...).String() + if got != c.want { + t.Fatalf("Get(%q) = %q, 期望 %q", c.path, got, c.want) + } + // 与 reflux 现有实现逐条对齐 + if want := newLegacy(o).Get(c.path...).String(); want != got { + t.Fatalf("Get(%q): fastx=%q reflux=%q, 两者行为不一致", c.path, got, want) + } + }) + } +} + +func TestGetMissing(t *testing.T) { + o := newUxOuter() + r := New(o) + + missing := [][]string{ + {"NoSuchField"}, + {"Mid", "Inner", "NoSuchField"}, + {"Tags", "99"}, + {"Tags", "notanumber"}, + {"Nums", "5"}, + {"Meta", "nokey"}, + {"Name", "City"}, // 在标量上继续取路径 + } + for _, p := range missing { + got := r.Get(p...) + if got.Exists() { + t.Fatalf("Get(%q) 不应该存在, 却返回了 %v", p, got.Any()) + } + if got.String() != "" { + t.Fatalf("Get(%q).String() 应该是空串, 得到 %q", p, got.String()) + } + } +} + +// 未导出字段既不能读也不能写 —— 这是 unsafe 路径上最关键的安全断言。 +func TestUnexportedFieldIsUnreachable(t *testing.T) { + o := newUxOuter() + r := New(o) + + if r.Get("secret").Exists() { + t.Fatal("未导出字段 secret 不应该可读") + } + + err := uxMustPanic(t, func() { r.Set("secret", "hacked") }) + if err == nil { + t.Fatal("向未导出字段写入应该 panic") + } + if o.secret != "should-not-be-reachable" { + t.Fatalf("未导出字段被改写了: %q", o.secret) + } +} + +// --------------------------------------------------------------------------- +// Set +// --------------------------------------------------------------------------- + +func TestUnsafeSet(t *testing.T) { + o := newUxOuter() + r := New(o) + + r.Set("Name", "changed") + if o.Name != "changed" { + t.Fatalf("Set(Name) 失败: %q", o.Name) + } + + r.Set("Mid.Inner.City", "Hangzhou") + if o.Mid.Inner.City != "Hangzhou" { + t.Fatalf("Set 嵌套字段失败: %q", o.Mid.Inner.City) + } + + r.Set("Mid.Inner.Zip", 330000) + if o.Mid.Inner.Zip != 330000 { + t.Fatalf("Set int 失败: %d", o.Mid.Inner.Zip) + } + + r.Set("Mid.Inner.Active", false) + if o.Mid.Inner.Active { + t.Fatal("Set bool 失败") + } + + r.Set("Mid.Inner.Ratio", 2.5) + if o.Mid.Inner.Ratio != 2.5 { + t.Fatalf("Set float64 失败: %v", o.Mid.Inner.Ratio) + } + + // 通过指针字段设置 + r.Set("Mid.InnerPtr.City", "Nanjing") + if o.Mid.InnerPtr.City != "Nanjing" { + t.Fatalf("经指针 Set 失败: %q", o.Mid.InnerPtr.City) + } + + // slice 元素 + r.Set("Tags.0", "z") + if o.Tags[0] != "z" { + t.Fatalf("Set slice 元素失败: %v", o.Tags) + } + + // array 元素 + r.Set("Nums.1", 99) + if o.Nums[1] != 99 { + t.Fatalf("Set array 元素失败: %v", o.Nums) + } + + // map 键(新增 + 覆盖) + r.Set("Meta.k1", "v1-new") + r.Set("Meta.k3", "v3") + if o.Meta["k1"] != "v1-new" || o.Meta["k3"] != "v3" { + t.Fatalf("Set map 键失败: %v", o.Meta) + } + + // slice 里的 struct 字段 + r.Set("Structs.0.City", "Changsha") + if o.Structs[0].City != "Changsha" { + t.Fatalf("Set slice 内 struct 字段失败: %v", o.Structs) + } +} + +// 类型不完全一致时应该走 cast 回退, 与 reflux 行为一致 +func TestUnsafeSetTypeConversion(t *testing.T) { + o := newUxOuter() + r := New(o) + + r.Set("Mid.Inner.Zip", "123456") // string -> int + if o.Mid.Inner.Zip != 123456 { + t.Fatalf("string->int 转换失败: %d", o.Mid.Inner.Zip) + } + + r.Set("Name", 42) // int -> string + if o.Name != "42" { + t.Fatalf("int->string 转换失败: %q", o.Name) + } + + r.Set("Mid.Inner.Ratio", 3) // int -> float64 + if o.Mid.Inner.Ratio != 3 { + t.Fatalf("int->float64 转换失败: %v", o.Mid.Inner.Ratio) + } +} + +func TestUnsafeSetErrors(t *testing.T) { + o := newUxOuter() + r := New(o) + + // 路径不存在 + if err := uxMustPanic(t, func() { r.Set("NoSuchField", 1) }); err == nil { + t.Fatal("向不存在的字段写入应该 panic") + } + // 父路径不存在 + if err := uxMustPanic(t, func() { r.Set("NoSuch.Deep.Path", 1) }); err == nil { + t.Fatal("父路径不存在时应该 panic") + } + // 空路径 + if err := uxMustPanic(t, func() { r.Set("", 1) }); err == nil { + t.Fatal("空路径应该 panic") + } + // slice 越界 + if err := uxMustPanic(t, func() { r.Set("Tags.99", "x") }); err == nil { + t.Fatal("slice 越界应该 panic") + } + // 无法转换的类型 + if err := uxMustPanic(t, func() { r.Set("Mid.Inner.Zip", struct{}{}) }); err == nil { + t.Fatal("无法转换的类型应该 panic") + } +} + +// 从 map 里取出来的 struct 是副本, 对它的写入不应该影响原 map —— +// 这与 reflect 里 "map 元素不可寻址" 的语义一致。 +func TestMapValueIsCopy(t *testing.T) { + o := newUxOuter() + r := New(o) + + got := r.Get("Objs", "o1") + if !got.Exists() { + t.Fatal("Objs.o1 应该存在") + } + err := uxMustPanic(t, func() { got.Set("City", "Modified") }) + if err == nil { + t.Log("对 map 元素副本的写入未 panic(落在副本上)") + } + if o.Objs["o1"].City != "Chengdu" { + t.Fatalf("原 map 里的值被改写了: %q", o.Objs["o1"].City) + } +} + +// 传值(非指针)时不应该影响调用方的原始数据 +func TestNewByValueDoesNotMutateOriginal(t *testing.T) { + o := *newUxOuter() + r := New(o) + r.Set("Name", "changed") + if o.Name != "root" { + t.Fatalf("传值构造时原始数据被改写了: %q", o.Name) + } + if r.Get("Name").String() != "changed" { + t.Fatal("副本上的写入没生效") + } +} + +// --------------------------------------------------------------------------- +// Accessor / JSON / 其它 R 方法 +// --------------------------------------------------------------------------- + +func TestAccessors(t *testing.T) { + o := newUxOuter() + r := New(o) + + if got := r.Get("Mid.Inner.City").String(); got != "Beijing" { + t.Fatalf("String() = %q", got) + } + if got := r.Get("Mid.Inner.Zip").Int(); got != 100000 { + t.Fatalf("Int() = %d", got) + } + if got := r.Get("Mid.Inner.Zip").Int64(); got != 100000 { + t.Fatalf("Int64() = %d", got) + } + if got := r.Get("Mid.Inner.Active").Bool(); !got { + t.Fatal("Bool() = false") + } + if got := r.Get("Mid.Inner.Ratio").Float64(); got != 1.5 { + t.Fatalf("Float64() = %v", got) + } + if got := r.Get("Mid.Inner.Ratio").Float32(); got != 1.5 { + t.Fatalf("Float32() = %v", got) + } + if got := r.Get("Mid.Inner.Zip").String(); got != "100000" { + t.Fatalf("int 转 String() = %q", got) + } + + // Raw / Ptr + raw := r.Get("Mid.Inner.City").Raw() + if !raw.IsValid() || raw.Kind() != reflect.String || raw.String() != "Beijing" { + t.Fatalf("Raw() = %v", raw) + } + ptr, ok := r.Get("Mid.Inner.City").Ptr().(*string) + if !ok || *ptr != "Beijing" { + t.Fatalf("Ptr() = %#v", r.Get("Mid.Inner.City").Ptr()) + } + // Ptr 拿到的必须是原始数据的地址, 改它能反映到原对象上 + *ptr = "ViaPtr" + if o.Mid.Inner.City != "ViaPtr" { + t.Fatal("Ptr() 返回的不是原始数据的地址") + } + + // Any + if got := r.Get("Mid.Inner.Zip").Any(); got != 100000 { + t.Fatalf("Any() = %#v", got) + } + + // StringSlice / Slice + if got := r.Get("Tags").StringSlice(); !reflect.DeepEqual(got, []string{"a", "b", "c"}) { + t.Fatalf("StringSlice() = %v", got) + } + if got := r.Get("Meta").StringMapString(); got["k1"] != "v1" { + t.Fatalf("StringMapString() = %v", got) + } +} + +func TestExistsKeysArray(t *testing.T) { + o := newUxOuter() + r := New(o) + + if !r.Exists("Mid", "Inner", "City") { + t.Fatal("Exists 应该为 true") + } + if r.Exists("Mid", "Nope") { + t.Fatal("Exists 应该为 false") + } + + keys := r.Keys() + if len(keys) == 0 || keys[0] != "Name" { + t.Fatalf("Keys() = %v", keys) + } + + arr := r.Get("Tags").Array() + if len(arr) != 3 || arr[2].String() != "c" { + t.Fatalf("Array() = %v", arr) + } + // Array 返回的元素应该指向原始底层数组, 对它的写入能反映到原对象上 + objs := r.Get("Structs").Array() + if len(objs) != 2 { + t.Fatalf("Structs.Array() 长度 = %d", len(objs)) + } + objs[0].Set("City", "Guiyang") + if o.Structs[0].City != "Guiyang" { + t.Fatalf("Array() 元素没有指向原始底层数组: %+v", o.Structs) + } +} + +func TestLookup(t *testing.T) { + o := newUxOuter() + r := New(o) + + acc, ok := r.Lookup("Mid.Inner.City") + if !ok || acc.String() != "Beijing" { + t.Fatalf("Lookup = %v %v", acc, ok) + } + if _, ok := r.Lookup("Nope"); ok { + t.Fatal("Lookup 不存在的路径应该返回 false") + } + if got := r.MustLookup("Nope").String(); got != "" { + t.Fatalf("MustLookup 不存在时应该返回零值, 得到 %q", got) + } +} + +func TestJSON(t *testing.T) { + o := newUxOuter() + r := New(o) + + b, err := json.Marshal(r.Get("Mid", "Inner")) + if err != nil { + t.Fatalf("Marshal 失败: %v", err) + } + if !strings.Contains(string(b), `"City":"Beijing"`) { + t.Fatalf("Marshal 结果不对: %s", b) + } + + target := r.Get("Mid", "Inner") + if err := json.Unmarshal([]byte(`{"City":"Kunming","Zip":650000}`), target); err != nil { + t.Fatalf("Unmarshal 失败: %v", err) + } + if o.Mid.Inner.City != "Kunming" || o.Mid.Inner.Zip != 650000 { + t.Fatalf("Unmarshal 没有写回原对象: %+v", o.Mid.Inner) + } +} + +// --------------------------------------------------------------------------- +// TypeDescriptor 本身 +// --------------------------------------------------------------------------- + +type uxSelfRef struct { + Name string + Next *uxSelfRef + Kids []uxSelfRef +} + +// 自引用类型不能让描述符构建无限递归 +func TestSelfReferentialType(t *testing.T) { + done := make(chan struct{}) + go func() { + defer close(done) + td := rfxDescriptorOf(reflect.TypeOf(uxSelfRef{})) + if len(td.fields) != 3 { + t.Errorf("字段数 = %d", len(td.fields)) + } + }() + <-done + + root := &uxSelfRef{Name: "a", Next: &uxSelfRef{Name: "b", Next: &uxSelfRef{Name: "c"}}} + r := New(root) + if got := r.Get("Next.Next.Name").String(); got != "c" { + t.Fatalf("自引用类型路径遍历失败: %q", got) + } +} + +// --------------------------------------------------------------------------- +// 并发 +// --------------------------------------------------------------------------- + +type uxRaceA struct { + X string + B uxRaceB +} +type uxRaceB struct { + Y string + C uxRaceC +} +type uxRaceC struct{ Z string } + +// 多个 goroutine 同时构建描述符 + 读写各自独立的实例 +func TestConcurrentAccess(t *testing.T) { + const n = 32 + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func(i int) { + defer wg.Done() + // 每个 goroutine 操作自己的实例, 但共享同一份全局描述符缓存 + a := &uxRaceA{X: "x", B: uxRaceB{Y: "y", C: uxRaceC{Z: "z"}}} + r := New(a) + for j := 0; j < 200; j++ { + if got := r.Get("B.C.Z").String(); got != "z" && got != "changed" { + t.Errorf("并发读到了意外的值: %q", got) + return + } + r.Set("B.C.Z", "changed") + r.Set("B.C.Z", "z") + } + // 也并发构建一些新类型的描述符 + switch i % 4 { + case 0: + rfxDescriptorOf(reflect.TypeOf(uxOuter{})) + case 1: + rfxDescriptorOf(reflect.TypeOf(uxSelfRef{})) + case 2: + rfxDescriptorOf(reflect.TypeOf(uxMiddle{})) + case 3: + rfxDescriptorOf(reflect.TypeOf(uxOuter{})) + } + }(i) + } + wg.Wait() +} + +// 同一个实例被多个 goroutine 并发只读, 不应该有竞争 +func TestConcurrentReadSameInstance(t *testing.T) { + a := &uxRaceA{X: "x", B: uxRaceB{Y: "y", C: uxRaceC{Z: "z"}}} + r := New(a) + + var wg sync.WaitGroup + for i := 0; i < 16; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 500; j++ { + if got := r.Get("B", "C", "Z").String(); got != "z" { + t.Errorf("并发只读得到 %q", got) + return + } + } + }() + } + wg.Wait() +} + +// --------------------------------------------------------------------------- + +func uxMustPanic(t *testing.T, f func()) (recovered any) { + t.Helper() + defer func() { recovered = recover() }() + f() + return nil +} diff --git a/unsafeptr.go b/unsafeptr.go new file mode 100644 index 0000000..6614f87 --- /dev/null +++ b/unsafeptr.go @@ -0,0 +1,125 @@ +package reflux + +import ( + "reflect" + "unsafe" +) + +// 本文件是整个 reflux 包里唯一使用 unsafe 的地方, 评审内存安全性时只需要看这里 +// 加上 value.go 里对这些函数的调用点。 +// +// 本包 unsafe 路径的核心不变式(invariant), 下面每个函数都依赖它: +// +// I1. value.ptr 永远是一个"指向某个 value.td.rtype 类型值的合法地址", +// 要么来自 reflect.Value.UnsafePointer()/reflect.New(), 要么由 +// fieldAt/elemAt 从一个满足 I1 的地址加上该类型真实的字段偏移量算出。 +// I2. ptr 是 unsafe.Pointer 类型而不是 uintptr, GC 能正常追踪它, +// 被指向的对象不会在 value 存活期间被回收或移动。 +// 所有指针运算都用 unsafe.Add 在单个表达式内完成, 不把地址存成 uintptr。 +// I3. 只有 Exported == true 的字段才允许构造出 value, 见 rfxFieldDesc.Exported +// 的注释: reflect.NewAt 不带只读标记, 语言层面的未导出字段保护在这里失效, +// 必须由调用方自己拦住。 +// I4. 只有 writable == true 的 value 才允许写入, writable 的传播规则见 +// value.go 的 canWrite 说明。 + +// fieldAt 返回 base 所指 struct 里偏移量为 off 的字段地址。 +// 要求: base 满足 I1, off 来自 reflect.StructField.Offset。 +func fieldAt(base unsafe.Pointer, off uintptr) unsafe.Pointer { + return unsafe.Add(base, off) +} + +// loadPtr 把 p 当作 *T 读出来, 返回它指向的地址。 +// 要求: p 指向一个指针类型的字段。返回 nil 表示空指针。 +func loadPtr(p unsafe.Pointer) unsafe.Pointer { + return *(*unsafe.Pointer)(p) +} + +// sliceHeader 与运行时 slice 的内存布局一致, 用于零拷贝读取切片头。 +type sliceHeader struct { + Data unsafe.Pointer + Len int + Cap int +} + +// sliceLen 返回 p 所指切片的长度。要求 p 指向一个 slice 类型的值。 +func sliceLen(p unsafe.Pointer) int { + return (*sliceHeader)(p).Len +} + +// sliceElemAt 返回 p 所指切片第 i 个元素的地址, 越界返回 nil。 +// 要求: elemSize 是该切片元素类型的真实 Size()。 +func sliceElemAt(p unsafe.Pointer, i int, elemSize uintptr) unsafe.Pointer { + h := (*sliceHeader)(p) + if i < 0 || i >= h.Len { + return nil + } + return unsafe.Add(h.Data, uintptr(i)*elemSize) +} + +// arrayElemAt 返回 p 所指数组第 i 个元素的地址, 越界返回 nil。 +func arrayElemAt(p unsafe.Pointer, i int, n int, elemSize uintptr) unsafe.Pointer { + if i < 0 || i >= n { + return nil + } + return unsafe.Add(p, uintptr(i)*elemSize) +} + +// valueAt 把 (地址, 类型) 还原成一个可寻址的 reflect.Value, 用于回退到 +// 标准反射路径(类型转换、JSON、map 操作等)。 +// +// 安全性: 返回的 Value 是可寻址且可写的, 即使它对应的是未导出字段 —— +// 这正是 I3 存在的原因, 调用方必须已经确认过 Exported。 +func valueAt(td *rfxTypeDesc, p unsafe.Pointer) reflect.Value { + return reflect.NewAt(td.rtype, p).Elem() +} + +// ptrValueAt 把 (地址, 类型) 还原成一个 *T 的 reflect.Value。 +func ptrValueAt(td *rfxTypeDesc, p unsafe.Pointer) reflect.Value { + return reflect.NewAt(td.rtype, p) +} + +// addrOf 取一个可寻址 reflect.Value 的地址。要求 v.CanAddr()。 +func addrOf(v reflect.Value) unsafe.Pointer { + return v.Addr().UnsafePointer() +} + +// boxCopy 把一个不可寻址的值(典型来源: map 取值、interface 拆包)拷贝到 +// 新分配的堆内存上, 返回该副本的地址。副本与原值不共享内存, 因此对它的写入 +// 不会影响原容器 —— 这与 reflect 里 "map 元素不可寻址" 的语义一致。 +func boxCopy(v reflect.Value) unsafe.Pointer { + p := reflect.New(v.Type()) + p.Elem().Set(v) + return p.UnsafePointer() +} + +// ---- 基础类型的直接读写 ---- +// 这些函数通过 *T 做**有类型**的读写, 编译器会正常插入 GC 写屏障, +// 不存在漏掉写屏障导致对象被误回收的问题。unsafe 只体现在"地址是算出来的"。 + +func loadString(p unsafe.Pointer) string { return *(*string)(p) } +func storeString(p unsafe.Pointer, v string) { *(*string)(p) = v } + +func loadInt(p unsafe.Pointer) int { return *(*int)(p) } +func storeInt(p unsafe.Pointer, v int) { *(*int)(p) = v } + +// 各宽度整数的直读。读取只依赖字段的 Kind(内存宽度), 不要求具名类型完全一致 —— +// type Age int 和 int 的内存表示相同, 按 *int 读出来的值是一样的。 +// (写入则不同, 见 storeFast: 写要求类型完全一致, 否则可能绕过 cast 的转换语义。) +func loadInt8(p unsafe.Pointer) int8 { return *(*int8)(p) } +func loadInt16(p unsafe.Pointer) int16 { return *(*int16)(p) } +func loadInt32(p unsafe.Pointer) int32 { return *(*int32)(p) } +func loadInt64(p unsafe.Pointer) int64 { return *(*int64)(p) } + +func loadUint(p unsafe.Pointer) uint { return *(*uint)(p) } +func loadUint8(p unsafe.Pointer) uint8 { return *(*uint8)(p) } +func loadUint16(p unsafe.Pointer) uint16 { return *(*uint16)(p) } +func loadUint32(p unsafe.Pointer) uint32 { return *(*uint32)(p) } +func loadUint64(p unsafe.Pointer) uint64 { return *(*uint64)(p) } + +func loadFloat32(p unsafe.Pointer) float32 { return *(*float32)(p) } + +func loadBool(p unsafe.Pointer) bool { return *(*bool)(p) } +func storeBool(p unsafe.Pointer, v bool) { *(*bool)(p) = v } + +func loadFloat64(p unsafe.Pointer) float64 { return *(*float64)(p) } +func storeFloat64(p unsafe.Pointer, v float64) { *(*float64)(p) = v } diff --git a/util.go b/util.go index 027d4ef..6885b73 100644 --- a/util.go +++ b/util.go @@ -350,13 +350,23 @@ func tryMapFieldKey(m reflect.Value, key string) reflect.Value { keyType := m.Type().Key() + // 这里必须把 key 复制到一个**新的局部变量**里,而且不能复用参数名。 + // + // reflect.ValueOf 会把字符串装箱进 interface,逃逸分析因此判定传进来的变量 + // 逃逸到堆。而 key 是从调用方的路径参数里切出来的子串 —— 一旦参数本身被判 + // 定为 leaking param,Get("Address","City") 里那个可变参数切片就必须堆分配, + // 即使这次调用根本没走到 map 分支。 + // 复制一次切断数据流,代价只落在真正访问 map 的路径上(那条路径本来就要为 + // 取出来的 map 元素分配内存)。 + k := strings.Clone(key) + // 构造原始 key var mapKey reflect.Value if keyType.Kind() == reflect.String { - mapKey = reflect.ValueOf(key) + mapKey = reflect.ValueOf(k) } else { // 非 string 键,尽量使用原始字符串构造可转换的 key - raw := reflect.ValueOf(key) + raw := reflect.ValueOf(k) if raw.Type().AssignableTo(keyType) { mapKey = raw } else if raw.Type().ConvertibleTo(keyType) { @@ -373,8 +383,9 @@ func tryMapFieldKey(m reflect.Value, key string) reflect.Value { } // 如果键类型是 string 且首字母大写,尝试首字母小写版本 - if keyType.Kind() == reflect.String && len(key) > 0 && unicode.IsUpper(rune(key[0])) { - lowercased := lowercaseFirst(key) + // (这里同样用副本 k,避免 key 被判定逃逸) + if keyType.Kind() == reflect.String && len(k) > 0 && unicode.IsUpper(rune(k[0])) { + lowercased := lowercaseFirst(k) mapKey = reflect.ValueOf(lowercased) val = m.MapIndex(mapKey) if val.IsValid() {