起因是想给 New 加一个跳过深拷贝的开关。做出来之后对比发现,现有的
"传指针"已经在三个维度上都更好, 那个开关是多余的 API:
New(m, WithRef()) New(&m)
速度 47.5ns / 3 allocs 24.8ns / 1 alloc
map/slice 共享底层 是 是
struct 写回调用方 否 是
新增 API 一个选项 + 一套语义 零
WithRef 对 struct 还有个说不清的半吊子语义 —— 共享 map/slice 字段的底层数据,
但写不回 struct 本身。这种"部分穿透"很难讲明白也容易踩坑, 而 & 没有这个问题。
所以撤掉选项, 改为把这件事在文档里讲透。
README 新增"该传值还是传指针"一节, 给出实测对比:
10 键嵌套 map 传值 3227ns/119allocs 传指针 24.8ns/1alloc 130x
50 元素 slice 传值 594ns/ 6allocs 传指针 25ns/1alloc 24x
8 字段纯值 struct 传值 85.5ns/ 2allocs 传指针 25.5ns/1alloc 3.4x
判断标准就一句: 这份数据我会不会通过 R 去写? 写了穿透出去有没有问题?
只读或本来就想改原数据就传指针; 需要互不干扰的副本就传值(或传指针后 Scope)。
另附取不到地址的表达式该怎么落局部变量。
代码改动只剩一处无关的重构: 把 cloneForValueInput 里的浅拷贝抽成 shallowBox。
新增 TestPointerInputSharesAndSkipsClone 把 README 的承诺钉住:
传指针必须共享底层 map/slice、能写回 struct 变量、不触发深拷贝,
且 Scope 仍能在此基础上取到隔离副本。
176 lines
5.2 KiB
Go
176 lines
5.2 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 可以是指针或非指针类型
|
|
// - 如果传入指针: 将直接使用该指针,可以修改原始数据,且不产生任何复制
|
|
// - 如果传入值: 会自动创建一个深度克隆的指针副本,修改不影响原始数据
|
|
//
|
|
// 深度克隆的代价随数据规模增长(10 键嵌套 map 约 3200 ns / 119 allocs,
|
|
// 而传指针只要 25 ns / 1 alloc)。数据量大、或者本来就想改调用方的数据时,
|
|
// **传指针**。取不到地址的表达式先落一个局部变量即可。
|
|
//
|
|
// 支持的类型: 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)
|
|
}
|
|
|
|
// shallowBox 把一个不可寻址的值装到新分配的内存里,只做逐字节复制。
|
|
// map/slice 复制的是头部,底层数据仍与调用方共享。
|
|
func shallowBox(v reflect.Value) reflect.Value {
|
|
box := reflect.New(v.Type())
|
|
box.Elem().Set(v)
|
|
return box
|
|
}
|
|
|
|
// 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 {
|
|
return shallowBox(v)
|
|
}
|
|
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,
|
|
}
|
|
}
|