diff --git a/docs/flow.md b/docs/flow.md index 3456106..173b5a8 100644 --- a/docs/flow.md +++ b/docs/flow.md @@ -155,6 +155,7 @@ | Map | 纯 map,单跳 | 120.0 ns / 8 allocs | 126.8 ns / 7 allocs | **0.95×** | | Map | map → struct → struct | 224.6 ns / 11 allocs | 140.3 ns / 6 allocs | 1.60× | | Map | map → map → struct → struct | 303.2 ns / 16 allocs | 205.6 ns / 10 allocs | 1.47× | +| Scope | `Scope("A")` 之后再取值 | 144.6 ns / 7 allocs | 48.7 ns / 2 allocs | 2.97× | 两点说明: diff --git a/rfx.go b/rfx.go index cc634b7..8c7d336 100644 --- a/rfx.go +++ b/rfx.go @@ -641,13 +641,25 @@ func (v rfx) slow() R { return &refx{value: ptrValueAt(v.td, v.ptr)} } +// Scope 返回指定路径的深度克隆,在克隆上的修改不影响原数据。 +// +// 注意返回的必须是 *rfx 而不是 refx: Scope 的结果通常还要继续 Get/Set, +// 交回慢实现的话后续操作就全部退回逐次反射了 —— 实测那样比直接 Get 慢 2.5 倍。 func (v *rfx) Scope(p ...string) R { - target := v.Get(p...) - tv, ok := target.(*rfx) - if !ok || !tv.valid() { + if !v.valid() { return Nil } - return tv.slow().Scope() + target, ok := v.walk(newPathIter(p)) + if !ok { + return Nil + } + // cloneForValueInput 与 New(值) 同一套逻辑: 含引用成分才递归深拷贝, + // 纯值类型逐字节复制就已经独立了。两者都返回指向副本的指针。 + cloned := cloneForValueInput(valueAt(target.td, target.ptr)) + if !cloned.IsValid() { + return Nil + } + return newRfx(cloned) } func (v *rfx) Append(items ...any) R { diff --git a/rfx_scope_test.go b/rfx_scope_test.go new file mode 100644 index 0000000..748b3f9 --- /dev/null +++ b/rfx_scope_test.go @@ -0,0 +1,144 @@ +package reflux + +import ( + "encoding/json" + "fmt" + "testing" +) + +// Scope 必须返回快实现(*rfx), 否则它之后的所有 Get/Set 都退回逐次反射。 +// 这一条很容易在重构里悄悄失守: 交回 refx 功能完全正常, 只是慢 3 倍, +// 测试不看类型就发现不了。 +func TestScopeReturnsFastImpl(t *testing.T) { + type leaf struct{ V string } + type doc struct { + A leaf + Tags []string + M map[string]any + } + d := &doc{A: leaf{V: "v"}, Tags: []string{"t"}, M: map[string]any{"k": "v"}} + r := New(d) + + for _, p := range [][]string{nil, {"A"}, {"Tags"}, {"M"}} { + got := r.Scope(p...) + if _, ok := got.(*rfx); !ok { + t.Fatalf("Scope(%v) 返回了 %T, 应当是 *rfx —— 慢实现会让后续操作退回逐次反射", p, got) + } + // 链下去也必须还是快实现 + if inner := got.Get("V"); p != nil && len(p) == 1 && p[0] == "A" { + if _, ok := inner.(*rfx); !ok { + t.Fatalf("Scope(%v).Get() 返回了 %T", p, inner) + } + } + } +} + +// 语义不能变: Scope 仍然是深度克隆, 改副本不影响原数据 +func TestScopeStillIsolates(t *testing.T) { + type inner struct { + Name string + Tags []string + } + type doc struct { + In inner + M map[string]string + Pure struct{ A, B string } + } + mk := func() *doc { + return &doc{ + In: inner{Name: "n", Tags: []string{"t0", "t1"}}, + M: map[string]string{"k": "v"}, + Pure: struct{ A, B string }{"a", "b"}, + } + } + + t.Run("含引用的子树", func(t *testing.T) { + d := mk() + s := New(d).Scope("In") + s.Set("Name", "changed") + s.Set("Tags.0", "changed") + if d.In.Name != "n" || d.In.Tags[0] != "t0" { + t.Fatalf("原数据被改写了: %+v", d.In) + } + if s.Get("Name").String() != "changed" || s.Get("Tags.0").String() != "changed" { + t.Fatal("副本上的写入没生效") + } + }) + + t.Run("纯值子树(走浅拷贝分支)", func(t *testing.T) { + d := mk() + s := New(d).Scope("Pure") + s.Set("A", "changed") + if d.Pure.A != "a" { + t.Fatalf("原数据被改写了: %+v", d.Pure) + } + }) + + t.Run("map 子树", func(t *testing.T) { + d := mk() + s := New(d).Scope("M") + s.Set("k", "changed") + if d.M["k"] != "v" { + t.Fatalf("原 map 被改写了: %v", d.M) + } + }) + + t.Run("无参数克隆整个对象", func(t *testing.T) { + d := mk() + s := New(d).Scope() + s.Set("In.Name", "changed") + if d.In.Name != "n" { + t.Fatalf("原数据被改写了: %+v", d.In) + } + }) +} + +// 与旧实现逐格对拍 +func TestScopeMatchesLegacy(t *testing.T) { + type inner struct { + Name string + Tags []string + } + type doc struct { + In inner + M map[string]any + S []inner + } + mk := func() *doc { + return &doc{ + In: inner{Name: "n", Tags: []string{"t"}}, + M: map[string]any{"k": map[string]any{"deep": "d"}}, + S: []inner{{Name: "s0"}}, + } + } + + paths := [][]string{ + nil, {"In"}, {"In", "Name"}, {"In", "Tags"}, {"In", "Tags", "0"}, + {"M"}, {"M", "k"}, {"M", "k", "deep"}, + {"S"}, {"S", "0"}, {"S", "0", "Name"}, + {"nosuch"}, {"In", "nosuch"}, + } + ops := []struct { + name string + f func(R) any + }{ + {"String", func(r R) any { return r.String() }}, + {"Exists", func(r R) any { return r.Exists() }}, + {"Keys", func(r R) any { return fmt.Sprintf("%v", r.Keys()) }}, + {"RawKind", func(r R) any { return r.Raw().Kind().String() }}, + {"JSON", func(r R) any { b, e := json.Marshal(r); return fmt.Sprintf("%s/%v", b, e) }}, + {"再取一层", func(r R) any { return r.Get("Name").String() }}, + } + + for _, p := range paths { + for _, op := range ops { + t.Run(fmt.Sprintf("%v/%s", p, op.name), func(t *testing.T) { + want := call(newLegacy(mk()).Scope(p...), op.f) + got := call(New(mk()).Scope(p...), op.f) + if want != got { + t.Fatalf("行为不一致\n 旧 = %s\n 新 = %s", want, got) + } + }) + } + } +}