Files
reflux/reflux.go
T
what a2c0c9783e perf: New(值) 对不含引用成分的类型改走浅拷贝
New(值) 的语义是"改副本不影响调用方的原数据", 原来一律走 DeepClone 递归。
但对于不含引用成分的类型, 逐字节拷贝就已经完全独立了, 没必要递归。

在类型描述符里加 needsClone(构建期算一次, 不在热路径):
含指针/切片/map/interface/chan/func/unsafe.Pointer 即为真, 递归包含字段与
数组元素。字符串不算 —— 底层数组不可变, 共享是安全的。

  New(纯值 struct)   145.0ns -> 85.5ns   1.70x
  New(含引用 struct) 304.9ns             不变(深拷贝确实必要)
  New(&ptr)           25.8ns             不变

注意 1.70x 而不是更多: New 还要做类型断言、normalizeInputValue、递归解引用、
类型校验和描述符查找, 省掉的只是深拷贝那一段(约 59ns)。

needsClone 必须在递归之前按 Kind 置位, 否则自引用类型会漏标: 类型图里的环
一定要经过某种间接(指针/切片/map), 先置位才能让递归回来的自引用类型读到
正确的值。测试里专门有一条 cNode{Next *cNode} 守这个。

判错的后果是静默的数据串改 —— 浅拷贝一个含引用的类型, 写入会穿透到调用方
的原数据, 不报错也不 panic。所以测试分两层:
  1. needsClone 判定本身: 14 个用例, 含"引用藏在两层嵌套下"、"藏在数组元素里"、
     自引用、time.Time(内含 *Location)等容易漏判的形态
  2. 隔离语义: 7 个用例, 无论走哪条路都断言原数据未被改写
  3. 与旧实现对拍: 5 种输入 x 5 个观察方法

已验证测试有效: 把 needsClone 强制为 false 后, 6 个隔离用例全部失败, 包括
藏在两层嵌套和数组元素里的引用。
2026-08-31 11:10:49 +08:00

165 lines
4.7 KiB
Go

package reflux
import (
"encoding/json"
"reflect"
"git.fsdpf.net/go/reflux/valuex"
)
// R 提供了一个统一的接口,用于访问和操作嵌套的结构体字段、切片元素和映射值
type R interface {
valuex.Accessor
json.Marshaler
json.Unmarshaler
// Get 通过路径获取嵌套字段的值,返回一个新的 T 实例
// 参数 p 为路径片段,例如 Get("user", "profile", "name")
Get(path ...string) R
// Scope 类似 Get,但用于创建一个指定路径的作用域视图
// 后续操作将基于这个作用域进行
Scope(p ...string) R
// Set 设置指定路径的值,支持链式调用
// 参数 key 为路径(支持点号分割),v 为要设置的值
// 返回当前 R 实例以支持链式调用
// 示例: rfx.Set("name", "Alice").Set("age", 30)
Set(key string, v any) R
// Append 追加指定路径的值
// 参数 items 为要追加的值
// 返回当前 R 实例以支持链式调用
Append(items ...any) R
// Delete 删除指定路径的值
// 参数 p 为路径片段
// 返回当前 R 实例以支持链式调用
Delete(p ...string) R
// Exists 检查指定路径的值是否存在
// 参数 p 为路径片段
// 返回 true 表示存在,false 表示不存在
Exists(p ...string) bool
// Array 将当前值转换为 R 切片
// 适用于数组或切片类型的值
Array() []R
// Keys 返回当前映射或结构体的所有键名
Keys() []string
}
// New 创建一个新的 R 实例
// 参数 v 可以是指针或非指针类型
// - 如果传入指针: 将直接使用该指针,可以修改原始数据
// - 如果传入值: 会自动创建一个深度克隆的指针副本,修改不影响原始数据
// 支持的类型: map、struct、slice、array
// 也支持 interface 类型以及部分基础类型(string/bool/float),会自动解析到实际类型
// 返回一个 R 接口实例,可用于访问和操作嵌套的字段、元素和键值对
func New(v any) R {
switch t := v.(type) {
case nil:
return Nil
case R:
return t
case []R:
// []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
}
}
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 = cloneForValueInput(actualValue)
}
return newRfx(rv)
}
// cloneForValueInput 为 New(值) 复制一份独立的数据,返回指向副本的指针。
//
// 只有类型含引用成分(指针/切片/map/interface 等)时才需要 DeepClone 的递归;
// 纯值类型逐字节拷贝就已经完全独立了 —— 字符串虽然内部有指针,但底层数组不可变,
// 共享是安全的,所以不算引用成分。
//
// 实测纯值 struct: DeepClone 145 ns / 2 allocs,浅拷贝 28 ns / 1 alloc。
// 判断本身不花钱: needsClone 在类型描述符构建时算好,这里只读一个 bool。
func cloneForValueInput(v reflect.Value) reflect.Value {
if td := rfxDescriptorOf(v.Type()); !td.needsClone {
box := reflect.New(v.Type())
box.Elem().Set(v)
return box
}
return DeepClone(v)
}
// 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,
}
}