diff --git a/reflux.go b/reflux.go index 9bab7a2..c3d1b4a 100644 --- a/reflux.go +++ b/reflux.go @@ -114,16 +114,31 @@ func New(v any) R { panic(NewErrUnsupportedTargetType(actualValue.Kind())) } - // 如果原始传入的不是指针类型,需要进行深度克隆以避免修改原始数据 - // 对于引用类型(map, slice)这尤其重要 + // 如果原始传入的不是指针类型,需要复制一份以避免修改原始数据 if !isPtr { - // 使用深度克隆创建一个完全独立的副本(DeepClone 返回的是指针) - rv = DeepClone(actualValue) + 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 { diff --git a/rfx_clone_test.go b/rfx_clone_test.go new file mode 100644 index 0000000..530cb8f --- /dev/null +++ b/rfx_clone_test.go @@ -0,0 +1,214 @@ +package reflux + +import ( + "reflect" + "testing" + "time" +) + +// New(值) 的语义是"改副本不影响调用方的原数据"。为了省掉深拷贝,现在会先看 +// 类型描述符里的 needsClone —— 不含引用成分的类型走逐字节浅拷贝。 +// +// 这一组测试守两件事: +// 1. needsClone 的判定不能漏(漏了就会浅拷贝一个含引用的类型,写入穿透到原数据) +// 2. 无论走哪条路,隔离语义都必须成立 +// +// 第 1 条是这次改动唯一的风险点:判错的后果是静默的数据串改,不报错也不 panic。 + +// ---- needsClone 判定 ---- + +type cPure struct { + A, B string + N int + F float64 + Ok bool + Arr [3]int + Sub struct{ X, Y string } +} + +type cSlice struct { + A string + Tags []string +} + +type cMap struct { + A string + Meta map[string]string +} + +type cPtr struct { + A string + P *string +} + +type cIface struct { + A string + V any +} + +// 引用成分藏在两层嵌套之下 +type cInner struct{ Tags []string } +type cMid struct{ Inner cInner } +type cDeepRef struct { + A string + Mid cMid +} + +// 引用成分藏在数组元素里 +type cArrRef struct { + A string + Arrs [2]cInner +} + +// 自引用 +type cNode struct { + Name string + Next *cNode +} + +func TestNeedsCloneDetection(t *testing.T) { + cases := []struct { + name string + typ reflect.Type + want bool + }{ + {"纯值 struct", reflect.TypeOf(cPure{}), false}, + {"含切片", reflect.TypeOf(cSlice{}), true}, + {"含 map", reflect.TypeOf(cMap{}), true}, + {"含指针", reflect.TypeOf(cPtr{}), true}, + {"含 interface", reflect.TypeOf(cIface{}), true}, + {"引用藏在两层嵌套下", reflect.TypeOf(cDeepRef{}), true}, + {"引用藏在数组元素里", reflect.TypeOf(cArrRef{}), true}, + {"自引用类型", reflect.TypeOf(cNode{}), true}, + {"time.Time(内含 *Location)", reflect.TypeOf(time.Time{}), true}, + {"裸切片", reflect.TypeOf([]string{}), true}, + {"裸 map", reflect.TypeOf(map[string]string{}), true}, + {"纯值数组", reflect.TypeOf([3]int{}), false}, + {"字符串不算引用", reflect.TypeOf(""), false}, + {"纯值嵌套 struct", reflect.TypeOf(struct{ S struct{ A string } }{}), false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := rfxDescriptorOf(c.typ).needsClone; got != c.want { + t.Fatalf("needsClone = %v, 期望 %v", got, c.want) + } + }) + } +} + +// ---- 隔离语义:无论走哪条路都必须成立 ---- + +func TestNewByValueIsolation(t *testing.T) { + t.Run("纯值 struct(走浅拷贝)", func(t *testing.T) { + orig := cPure{A: "a", N: 1, Arr: [3]int{1, 2, 3}} + orig.Sub.X = "x" + r := New(orig) + r.Set("A", "changed") + r.Set("N", 99) + r.Set("Arr.0", 100) + r.Set("Sub.X", "changed") + if orig.A != "a" || orig.N != 1 || orig.Arr[0] != 1 || orig.Sub.X != "x" { + t.Fatalf("原数据被改写了: %+v", orig) + } + if r.Get("A").String() != "changed" || r.Get("Arr.0").Int() != 100 { + t.Fatal("副本上的写入没生效") + } + }) + + t.Run("含切片(走深拷贝)", func(t *testing.T) { + orig := cSlice{A: "a", Tags: []string{"x", "y"}} + r := New(orig) + r.Set("Tags.0", "changed") + if orig.Tags[0] != "x" { + t.Fatalf("原切片被改写了: %v", orig.Tags) + } + }) + + t.Run("含 map(走深拷贝)", func(t *testing.T) { + orig := cMap{A: "a", Meta: map[string]string{"k": "v"}} + r := New(orig) + r.Set("Meta.k", "changed") + if orig.Meta["k"] != "v" { + t.Fatalf("原 map 被改写了: %v", orig.Meta) + } + }) + + t.Run("引用藏在两层嵌套下", func(t *testing.T) { + orig := cDeepRef{A: "a", Mid: cMid{Inner: cInner{Tags: []string{"x"}}}} + r := New(orig) + r.Set("Mid.Inner.Tags.0", "changed") + if orig.Mid.Inner.Tags[0] != "x" { + t.Fatalf("深层的原切片被改写了: %v", orig.Mid.Inner.Tags) + } + }) + + t.Run("引用藏在数组元素里", func(t *testing.T) { + orig := cArrRef{A: "a"} + orig.Arrs[0] = cInner{Tags: []string{"x"}} + r := New(orig) + r.Set("Arrs.0.Tags.0", "changed") + if orig.Arrs[0].Tags[0] != "x" { + t.Fatalf("数组元素里的原切片被改写了: %v", orig.Arrs[0].Tags) + } + }) + + t.Run("裸 map", func(t *testing.T) { + orig := map[string]any{"k": "v"} + r := New(orig) + r.Set("k", "changed") + if orig["k"] != "v" { + t.Fatalf("原 map 被改写了: %v", orig) + } + }) + + t.Run("裸切片", func(t *testing.T) { + orig := []string{"x", "y"} + r := New(orig) + r.Set("0", "changed") + if orig[0] != "x" { + t.Fatalf("原切片被改写了: %v", orig) + } + }) +} + +// 与旧实现逐格对拍:浅拷贝路径的可观察行为必须与 DeepClone 完全一致。 +func TestNewByValueMatchesLegacy(t *testing.T) { + mk := []struct { + name string + val func() any + }{ + {"纯值 struct", func() any { return cPure{A: "a", N: 7, F: 1.5, Ok: true, Arr: [3]int{1, 2, 3}} }}, + {"纯值数组", func() any { return [3]int{1, 2, 3} }}, + {"含切片", func() any { return cSlice{A: "a", Tags: []string{"x"}} }}, + {"含 map", func() any { return cMap{A: "a", Meta: map[string]string{"k": "v"}} }}, + {"纯值嵌套", func() any { return struct{ S struct{ A string } }{} }}, + } + ops := []struct { + name string + f func(R) any + }{ + {"RawKind", func(r R) any { return r.Raw().Kind().String() }}, + {"Keys", func(r R) any { return r.Keys() }}, + {"JSON", func(r R) any { b, e := r.MarshalJSON(); return string(b) + "/" + errStr(e) }}, + {"Any", func(r R) any { return r.Any() }}, + {"PtrType", func(r R) any { return reflect.TypeOf(r.Ptr()).String() }}, + } + for _, m := range mk { + for _, op := range ops { + t.Run(m.name+"/"+op.name, func(t *testing.T) { + want := call(newLegacy(m.val()), op.f) + got := call(New(m.val()), op.f) + if want != got { + t.Fatalf("行为不一致\n 旧 = %s\n 新 = %s", want, got) + } + }) + } + } +} + +func errStr(e error) string { + if e == nil { + return "" + } + return e.Error() +} diff --git a/rfx_typedesc.go b/rfx_typedesc.go index 041c25b..66ebe3b 100644 --- a/rfx_typedesc.go +++ b/rfx_typedesc.go @@ -49,6 +49,16 @@ type rfxTypeDesc struct { // ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次 ptrElemKind reflect.Kind + // needsClone 表示该类型是否含有引用语义的成分(指针/切片/map/interface/ + // chan/func/unsafe.Pointer), 递归包含字段与元素。 + // + // New(值) 要保证"改副本不影响调用方的原数据"。对于不含任何引用成分的类型, + // 逐字节拷贝就已经完全独立了, 没必要走 DeepClone 的递归 —— 实测纯值 struct + // 深拷贝 145ns/2allocs, 而语义等价的浅拷贝只要 28ns/1alloc。 + // + // 字符串不算引用成分: 它的底层数组不可变, 共享是安全的。 + needsClone bool + // fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。 // 热路径上只要比较一个字节, 就能决定走不走零分配快路径。 fast fastKind @@ -161,6 +171,18 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType // 先登记再递归, 否则自引用类型会栈溢出 pending[t] = td + // needsClone 必须在递归之前按 Kind 置位。 + // + // 类型图里的环一定要经过某种间接(指针/切片/map), 而这几种在这里就被标成 + // true 了; 于是自引用类型递归回来时读到的是已经正确的值, 不会因为"还没算完" + // 而漏标。举例: type Node struct{ Next *Node } —— 处理 *Node 时先置 true, + // 再递归回 Node, Node 看到字段 needsClone=true, 自己也跟着为 true。 + switch t.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface, + reflect.Chan, reflect.Func, reflect.UnsafePointer: + td.needsClone = true + } + switch t.Kind() { case reflect.Struct: n := t.NumField() @@ -179,6 +201,10 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType for i := range td.fields { f := &td.fields[i] td.fieldMap[f.Name] = f + // 任一字段含引用成分, 整个 struct 就必须深拷贝 + if f.Type.needsClone { + td.needsClone = true + } } // 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。 // 只在不与真实字段名冲突时才登记。 @@ -204,6 +230,10 @@ func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxType td.Elem = buildLocked(t.Elem(), pending) td.elemSize = t.Elem().Size() td.arrayLen = t.Len() + // 数组是值语义, 但元素含引用成分时整体仍需深拷贝 + if td.Elem.needsClone { + td.needsClone = true + } case reflect.Map: // map 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型,