Roblox Guidebook知识库
简体中文 ⌄

开发 / ROBLOX

Luau 中的库存副本:为什么 table.clone 后的修改仍会影响原始数据

使用小型虚构库存,比较赋值、浅复制和分别复制记录。经过验证的示例展示如何修改草稿而保留原始数量,也说明这个两层数据模型的能力边界。

更新日期:

定义草稿应该怎样工作 #

设想教学库存有两种材料:copper 的 count=3,wood 的 count=1。你想打开编辑草稿,把草稿中的铜数量改成 9,同时让原始数据保留 3。需要一起检查两个值,单个面板的标签并不足够。先写下预期组合 original=3、draft=9,再选择能够提供这种分离的复制方法,不要只凭函数调用没有报错就判断任务完成。

材料以及 label、equipped 字段都是练习中的原创内容。它们是普通内存表,不是真实 Roblox 玩家的库存。我们在独立 Luau 中验证了逻辑,没有接入 GUI、服务器、RemoteEvent 或存档。这一边界让示例结果可以诚实说明,而不是被呈现成完整物品系统。复制检查通过,也不能证明已发布体验中向玩家发放了真实资源。

区分新变量名与新表 #

写 draft=original,只是给同一张表另一个名称。官方指南说明,表是通过引用访问的值;普通赋值不会创建独立快照。因此改变 draft.copper.count,也可以通过 original.copper.count 观察到。问题不在文字渲染:两个变量指向相同数据。在任何界面参与之前,所需的数据分离就已经不存在,不能靠刷新标签解决。

第一项检查使用新建的数据集,通过这个第二名称把数量设为 7。我们的独立测试中,alias==source 为真,source.copper.count 变成 7。这是共享对象的已验证例子。不要只因为变量名称不同就把它叫作副本。进入下一项检查之前,重新建立数量为 3 的数据集,避免前一次修改影响之后的比较,让每个结果都具有清楚的起点。

理解 table.clone 的边界 #

table.clone 创建新的外层表,但其内容是浅复制。如果某个值本来是另一张表,它的引用仍然共享。因此 draft~=original 可以为真,同时 draft.copper==original.copper 也为真。这并不矛盾:外层容器不同,但嵌套记录相同。数量编辑器必须检查修改发生的深度,而不是把外层引用不同当成所有数据完全独立的证明。

第二项独立检查通过浅副本把嵌套数量改为 8,原始数量也变成 8。可是,从外层副本删除 wood,并不会从原始容器删除 wood。两个操作影响不同层级,所以结果不同。不要把它们合并成含糊的“复制有效”或“复制无效”。写明检查的字段与层级,保留两个观察,而不是删掉不符合笼统结论的那个结果。

方法是否共享 copper 记录
赋值是
table.clone是
分别复制记录否

分别比较引用和值 #

count 回答的是储存了什么数量,使用 == 比较表回答的是当前示例中是否为同一张表。数字相同不能证明独立。两个不同记录在尚未修改时,都可能含有 3。因此先检查引用结构,再进行操作并比较两个最终值。否则共享错误会一直隐藏到实际编辑开始,而那个时候原始数据已经发生了你不希望的变化。

准备简短记录:外层表不同、copper 记录不同、草稿修改后原始值不变。为反方向再修改原始数据里的 wood,并比较草稿。如果编辑一个对象时另一个对象也可能更新,双向检查很有帮助。我们的测试执行这些比较,但没有测量并发网络行为,也不能证明体验中的所有冲突已经消除。结论应保持在实际检查的数据模型范围内。

从数据到草稿打开大图 ↗
有限两层辅助函数的原创示意图。

按明确的数据形状复制记录 #

原创 copyInventoryDraft 支持以非空字符串作为物品标识的字典。每个值是一张普通记录表,具名字段只能包含字符串、数字或布尔值。函数先复制外层容器,再分别复制每个记录。对于这个形状,数量、标签与 equipped=false 保留原值,但记录表不再与原始记录共享身份。数据形状属于函数设计的一部分,而不是事后追加的说明。

这个约定被刻意限制:元表、嵌套 metadata 表、函数以及数字字段键都不支持,并会明确拒绝。它不是任意数据的通用深复制。限制让示例易于理解和验证,不承诺处理循环或保留复杂引用图。如果项目结构不同,先描述它并选择复制规则,不要为了让代码运行成功就删掉检查,然后把结果误当作对所有结构都有效。

-- Teaching model only: string item IDs -> plain records -> scalar fields.
-- This is not a universal deep-copy function or a server validation layer.
local function copyInventoryDraft(original)
    assert(type(original) == "table", "Expected an inventory table")
    assert(getmetatable(original) == nil, "Root metatables are outside this model")
    local draft = table.clone(original)
    for id, record in pairs(original) do
        assert(type(id) == "string" and id ~= "", "Expected a nonempty item ID")
        assert(type(record) == "table", "Expected an item record")
        assert(getmetatable(record) == nil, "Record metatables are outside this model")
        for field, value in pairs(record) do
            assert(type(field) == "string", "Expected named record fields")
            local valueType = type(value)
            assert(valueType == "string" or valueType == "number" or valueType == "boolean",
                "Only scalar record fields are supported")
        end
        draft[id] = table.clone(record)
    end
    return draft
end

return copyInventoryDraft

运行短小的使用示例 #

在独立 Luau 中,把辅助函数保存为 table-copy-example.luau,把使用示例保存为同一文件夹中的 table-copy-usage.luau。第二个文件通过相对路径 require 获取函数,创建两项教学记录,把 draft.copper.count 改为 9。我们验证的是这种运行安排。移植到 Roblox ModuleScript 时,应另行检查模块访问方式与环境;相对路径文件不是完整 Studio 配置。

经过验证的输出为 GUIDEBOOK_TABLE_COPY_USAGE original=3 draft=9。Assertions 还比较两个数量,并确认 equipped=false 保留。文章展示完整的有限辅助函数及其简短应用,两者都不绘制界面,也不发放物品。输出不同时,请检查运行了哪个文件、函数来自哪里,以及原始数据是否早已被修改。保留这些上下文,不要只依赖一个熟悉的输出标签判断结果。

local copyInventoryDraft = require("./table-copy-example")
local original = {
    copper = {count = 3, label = "Copper", equipped = false},
    wood = {count = 1, label = "Wood", equipped = true},
}
local draft = copyInventoryDraft(original)
draft.copper.count = 9
assert(original.copper.count == 3)
assert(draft.copper.count == 9)
assert(draft.copper.equipped == false)
print("GUIDEBOOK_TABLE_COPY_USAGE original=" .. original.copper.count .. " draft=" .. draft.copper.count)

检查来自两边的修改 #

分别复制记录后,修改草稿的铜数量,原始数量仍为 3。随后修改原始 wood,草稿中之前的 wood 值不变。草稿外层的插入与删除,也不会在原始数据中插入或删除记录。这些观察适用于已测试的数据形状,比单次成功调用的消息更有意义,因为后者不说明之后的编辑是否仍然独立,更不能替代两边数值的比较。

如果两个原始标识引用同一记录,辅助函数会为每个标识创建独立副本。测试确认这些新记录不同,修改其中一个不会修改另一个。所以原先的共享关系没有保留,这是函数的有意特征。如果某项目需要共享记录的含义,就需要另一种策略。不要把这里的结果描述成所有对象图和身份关系的精确复制,或者假定引用拓扑一定与原始结构相同。

已验证的结果打开大图 ↗
独立 Luau 的结果,不是真实游戏库存。

不要把 freeze 当作深层独立 #

官方库说明,table.freeze 在原位置改变传入表的状态,不会自动冻结嵌套值。我们的测试冻结库存外层,检查里面的 copper 仍然可以修改,count 变化依旧可能。不可修改的外层容器,不能证明内部所有数据都禁止编辑,也不证明它们已脱离其他引用。这些是不同属性,需要针对各自条件观察,而不是只检查一个标记。

table.clone 冻结表后会返回未冻结的外层副本,而浅复制里的嵌套记录仍然共享。我们单独检查这一点,然后用有限辅助函数产生独立记录。不要用一个 frozen 标记证明结果的所有属性。先决定是否需要复制、保留共享引用或限制修改。这些要求不同,应通过不同动作检查,不能从同一个状态值推断整套行为。

保留独立验证的范围 #

检查涵盖赋值、浅复制、两层记录独立性、false、空字典、共享原始记录,以及明确不支持的数据形状。冻结外层容器的行为另行测试。不要推广到所有递归结构:辅助函数没有遍历那样的图。拒绝更深记录是它的约定,不是 Luau 或 Roblox 的通用错误,也不能说明提供了适用于任意对象图的复制库。

内存复制不证明玩家有权编辑真实库存,也不能替代服务器验证,更不会跨会话保存进度。界面还需要检查刷新、放弃草稿和接收新数据;持久存储需要另一个协议。本文没有执行它们。向其他开发者传递代码时,把这些边界与验证输出一起交付,避免有限辅助函数被误认为已经完成的游戏系统,或者省略原本必要的集成检查。

情况结果
修改草稿原始数量保持3
修改原始数据草稿不变
更深的对象按约定拒绝
GUI 与存档尚未测试

为具体动作选择复制方式 #

最终记录应说明复制了哪一层、哪些引用仍共享,以及哪些修改确实隔离。本练习中的 original=3、draft=9 符合预定编辑任务。但是,如果只需要一个引用现有对象的新列表,浅复制也可以是有意选择。先定义动作及其预期效果,再选择方法,不要自动把最复杂的复制方式用于每一份数据,而忽略项目真正需要保留的关系。

原创记录、辅助函数和测试可供学习与适配。把数据形状与限制一起保留;新增嵌套字段时,需要重新审查约定并运行对应检查。没有测量和集成测试时,不要承诺速度提升、内存节省或修复真实库存。请用官方来源核对 table.clone 与 table.freeze 的最新规则,并通过实际运行确认具体实现的结果,而不是仅凭一个方法名称推测其能力。

原始资料

Luau — Standard Library
Roblox Creator Hub — Tables