2.表引用与共享

2.表引用与共享


2.1 知识点

引用语义回顾

我们学习过 table 的基本用法。
table 直接赋值,不会复制出一张新表。

local a = { id = 1 }
local b = a

这段代码里只有一张表。
ab 是两个变量,它们指向同一张表。

b.id = 2

print(a.id) -- 输出:2
print(b.id) -- 输出:2

如果直接改表字段,两个变量都会看到变化。重点是:拿到的是独立数据,还是同一张表的另一个入口。

改字段和换引用

改字段和换引用是两种不同语义。

local a = { hp = 100 }
local b = a

b.hp = 80

print(a.hp) -- 输出:80

b.hp = 80 改的是原表字段。
只要还握着这张表,都会看到这个变化。

换引用则不会动旧表:

local a = { hp = 100 }
local b = a

b = { hp = 999 }

print(a.hp) -- 输出:100
print(b.hp) -- 输出:999

b 指向新表以后,a 还在旧表上。

参数传的是引用值

Lua 参数是按值传递。
但传进去的值如果是 table,传的就是地址。

local role = { hp = 100 }

local function Hurt(target)
    target.hp = target.hp - 20
end

Hurt(role)

print(role.hp) -- 输出:80

函数里改字段,改到的是外面那张表。

给参数重新赋值则不会影响外部变量:

local role = { hp = 100 }

local function Reset(target)
    target = { hp = 999 }
end

Reset(role)

print(role.hp) -- 输出:100

target 只是函数里的局部变量。
它换到新表,不会把外面的 role 一起换掉。

配置表和运行时表

配置表通常是静态共享数据。
如果业务直接拿配置改,容易污染原始配置。

local SkillConfig = {
    [1001] = {
        id = 1001,
        name = "FireBall",
        damage = 100
    }
}

local cfg = SkillConfig[1001]

cfg.damage = cfg.damage + 50

print(SkillConfig[1001].damage) -- 输出:150

这里改的是配置本身。
项目里一般会把配置和运行时数据拆开:

local SkillConfig = {
    [1001] = {
        id = 1001,
        name = "FireBall",
        damage = 100
    }
}

local cfg = SkillConfig[1001]

local runtimeSkill = {
    id = cfg.id,
    name = cfg.name,
    damage = cfg.damage,
    level = 1
}

runtimeSkill.damage = runtimeSkill.damage + 50

print(runtimeSkill.damage)      -- 输出:150
print(SkillConfig[1001].damage) -- 输出:100

配置只负责给初始值。
战斗过程、Buff 加成、临时修正,这些都必须要有运行时对象。

类表字段共享

Lua 里用表和元表模拟类时,类表上的 table 字段要注意,看下面这个case:

local PlayerClass = {
    buffs = {}
}

local playerA = {}
local playerB = {}

setmetatable(playerA, { __index = PlayerClass })
setmetatable(playerB, { __index = PlayerClass })

table.insert(playerA.buffs, "SpeedUp")

print(#playerA.buffs) -- 输出:1
print(#playerB.buffs) -- 输出:1

playerB 没加 Buff,两个实例都通过 __index 读到 PlayerClass.buffs,有点像静态变量。

但是实际上运行时实例状态应该放在实例表里:

local playerA = { buffs = {} }
local playerB = { buffs = {} }

table.insert(playerA.buffs, "SpeedUp")

print(#playerA.buffs) -- 输出:1
print(#playerB.buffs) -- 输出:0

否则可能一改改到了“静态”成员。

旧引用残留

旧引用可能在回调、缓存、模块返回值、协程里被捕获。

local function CreateHpGetter(target)
    return function()
        return target.hp
    end
end

local role = { hp = 100 }
local getHp = CreateHpGetter(role)

role = { hp = 999 }

print(role.hp) -- 输出:999
print(getHp()) -- 输出:100

上面的例子里,外部 role 已经换成新表,回调捕获的 target 还指着旧表。UI 按钮、事件、计时器、协程里都可能残留。


2.2 知识点代码

Lesson2_表引用与共享.lua

print("**********表引用与共享************")

print("**********知识点一 引用语义回顾************")

local a = { id = 1 }
local b = a

b.id = 2

print(a.id) -- 输出:2
print(b.id) -- 输出:2


print("**********知识点二 改字段和换引用************")

local dataA = { hp = 100 }
local dataB = dataA

dataB.hp = 80

print(dataA.hp) -- 输出:80,改字段会影响原表

dataB = { hp = 999 }

print(dataA.hp) -- 输出:80,dataA 还指向旧表
print(dataB.hp) -- 输出:999,dataB 已经指向新表


print("**********知识点三 参数传的是引用值************")

local role = { hp = 100 }

local function Hurt(target)
    target.hp = target.hp - 20
end

local function Reset(target)
    target = { hp = 999 }
end

Hurt(role)
print(role.hp) -- 输出:80,函数里改字段会影响原表

Reset(role)
print(role.hp) -- 输出:80,函数里换引用不会替换外部 role


print("**********知识点四 配置表别当运行时表改************")

local BadSkillConfig = {
    [1001] = {
        id = 1001,
        name = "FireBall",
        damage = 100
    }
}

local badCfg = BadSkillConfig[1001]
badCfg.damage = badCfg.damage + 50

print(BadSkillConfig[1001].damage) -- 输出:150,原始配置被改脏

local SkillConfig = {
    [1001] = {
        id = 1001,
        name = "FireBall",
        damage = 100
    }
}

local cfg = SkillConfig[1001]
local runtimeSkill = {
    id = cfg.id,
    name = cfg.name,
    damage = cfg.damage,
    level = 1
}

runtimeSkill.damage = runtimeSkill.damage + 50

print(runtimeSkill.damage)      -- 输出:150,只改运行时数据
print(SkillConfig[1001].damage) -- 输出:100,配置表保持原值


print("**********知识点五 类表字段共享************")

local PlayerClass = {
    buffs = {}
}

local playerA = {}
local playerB = {}

setmetatable(playerA, { __index = PlayerClass })
setmetatable(playerB, { __index = PlayerClass })

table.insert(playerA.buffs, "SpeedUp")

print(#playerA.buffs) -- 输出:1
print(#playerB.buffs) -- 输出:1,两个实例读到同一张类表字段

local fixedPlayerA = { buffs = {} }
local fixedPlayerB = { buffs = {} }

table.insert(fixedPlayerA.buffs, "SpeedUp")

print(#fixedPlayerA.buffs) -- 输出:1
print(#fixedPlayerB.buffs) -- 输出:0,实例状态已经分开


print("**********知识点六 旧引用藏在回调里************")

local function CreateHpGetter(target)
    return function()
        return target.hp
    end
end

local oldRole = { hp = 100 }
local getHp = CreateHpGetter(oldRole)

oldRole = { hp = 999 }

print(oldRole.hp) -- 输出:999,外部变量已经换到新表
print(getHp())    -- 输出:100,回调还握着旧表


print("**********知识点七 总结************")

print("配置表默认只读,运行时状态单独建")
print("类表字段别当实例状态")
print("热更排查旧回调、旧闭包、旧协程")


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏