14 环境
Lua 将所有的全局变量保存在一个常规的 table 中,这个 table 称为环境
Lua 将环境 table 自身保存在一个全局变量 _G
中
-- 打印当前环境中所有全局变量的名称
for n in pairs(_G) do
print(n)
end
具有动态名字的全局变量
如果已知变量名称要得到变量的值,以下写法效率不高:
x = 10
varname = "x"
print(loadstring("return " .. varname)())
直接使用环境 table:
print(_G[varname])
以下情况不能直接使用环境 table
x = {}
x.a = {}
x.a.b = {}
x.a.b.c = 90
varname = "x.a.b.c"
print(_G[varname]) -->nil
需要自己写函数
x = {}
varname = "x.a.b.c"
function getField(f)
local v = _G
for w in string.gmatch(f, "[%w_]+") do
v = v[w]
end
return v
end
function setField(f, v)
local t = _G
for w, d in string.gmatch(f, "([%w_]+)(%.?)") do
if d == "." then
t[w] = t[w] or {} -- 如果不存在就创建table
t = t[w]
else
t[w] = v
end
end
end
setField(varname, 99)
print(getField(varname))
全局变量声明
为了防止笔误,以下方法检测所有对全局 table 中不存在的 key 的访问:
setmetatable(_G, {
__newindex = function(_, n)
error("attempt to write to undeclared variable " .. n, 2)
end,
__index = function(_, n)
error("attempt to read undeclared variable " .. n, 2)
end,
})
a = 9 -->attempt to write to undeclared variable a
此时若要声明新的变量,可以使用 rawset 绕过元表
function declare(name, initval)
-- 确保新的全局变量的值不是nil
rawset(_G, name, initval or false)
end
setmetatable(_G, {
__newindex = function(_, n)
error("attempt to write to undeclared variable " .. n, 2)
end,
__index = function(_, n)
error("attempt to read undeclared variable " .. n, 2)
end,
})
declare("a", 9)
print(a)
以下的元表允许主程序块或者C代码对全局变量赋值,但不允许普通的 Lua 函数赋值
function declare(name, initval)
-- 确保新的全局变量的值不是nil
rawset(_G, name, initval or false)
end
setmetatable(_G, {
__newindex = function(t, n, v)
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attempt to write to undeclared variable " .. n, 2)
end
rawset(t, n, v)
end,
__index = function(_, n)
error("attempt to read undeclared variable " .. n, 2)
end,
})
a = 9 -->good
function foo()
b = 10
end
foo() -->error
测试变量是否存在:
setmetatable(_G, {
__newindex = function(t, n, v)
local w = debug.getinfo(2, "S").what
if w ~= "main" and w ~= "C" then
error("attempt to write to undeclared variable " .. n, 2)
end
rawset(t, n, v)
end,
__index = function(t, n)
error("attempt to read undeclared variable " .. n, 2)
end,
})
if rawget(_G, "a") == nil then
print("not exist") -->not exist
end
如果希望全局变量可以拥有 nil 值,可以引入一个辅助 table 记录所有已声明变量的名称,以确定变量是否已声明过,详见P128
非全局的环境
setfenv 可以改变一个函数的环境,参数是一个函数和新的环境 table,第一个参数为数字,表示函数调用栈:
function doNothing()
a = 1
setfenv(1, {g = _G})
g.print(g.a)
end
doNothing() -->1
print(a) -->1
也可以通过继承来组装新环境,这样做的好处是原来的 _G 得以保留
function doNothing()
a = 1
local newgt = {}
setmetatable(newgt, {__index = _G})
setfenv(1, newgt)
print(a) -->1
end
doNothing()
每个函数以及 closure 都有一个继承的环境
function factory()
return function()
return a
end
end
a = 3
f1 = factory()
f2 = factory()
print(f1())
setfenv(f1, {a = 10}) -- f1的环境被改变
print(f1())
print(f2())
如果一个程序块的环境被改变,后续由它创建的函数都将共享这个新环境
15 模块与包
require 函数
require 函数的行为如下所示:
function require(name)
if not package.loaded[name] then -- 1
local loader = findloader(name) -- 2
if loader == nil then
error("unable to load module " .. name)
end
package.loaded[name] = true -- 3
local res = loader(name) -- 4
if res ~= nil then
package.loaded[name] = res
end
end
return package.loaded[name]
end
- 在
package.loaded
中检查模块是否已被加载,如果是,直接返回相应的值(避免无限循环) - 如果还没被加载,先尝试找一个加载器,首先在
package.preload
中查找,大概率不会找到,接着尝试从 Lua 文件或 C 程序库中加载模块,分别通过loadfile
或者loadlib
来加载(注意这里只是加载,还没有运行)。如果找不到加载器,报错 - 在
package.loaded
中标记模块已加载,避免无限循环 - 运行代码,如果有返回值,丢入
package.loaded
写一个自定义的 module:
local modname = "myComplex"
local M = {}
_G[modname] = M
package.loaded[modname] = M
function M.new(r, i)
return {r = r, i = i}
end
M.i = M.new(0, 1)
function M.add(c1, c2)
return M.new(c1.r + c2.r, c1.i + c2.i)
end
function M.display(c)
print(string.format("(%d, %d)", c.r, c.i))
end
在主函数块中使用自定义 module
package.path = package.path .. ";C:\\Users\\Administrator\\Desktop\\LuaTests\\?.lua"
local complex = require "myComplex"
a = complex.new(2, 3)
b = complex.new(4, 3)
complex.display(complex.add(a, b)) -->(6, 6)
使用环境
local modname = "myComplex"
local M = {}
_G[modname] = M
package.loaded[modname] = M
setfenv(1, M) -- 让模块的主程序块有独占的环境
-- 这样声明成员的时候就不需要前缀M.
function new(r, i)
return {r = r, i = i}
end
i = new(0, 1)
function add(c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end
-- 以下函数暂时不可用,因为使用了原来的全局变量print和string
--function display(c)
-- print(string.format("(%d, %d)", c.r, c.i))
--end
如果要保留原来的全局变量,有以下 3 种办法:
- 设置环境之前
setmetatble(M, {__index = _G})
,即把环境 table 保存在__index
元方法中 - 更快捷的方法是,设置环境之前
local _G = _G
,用一个局部变量保存之前的环境 table,之后如果需要使用类似print
的函数,需要写成_G.print
。更快是因为相比上一种方法,没有涉及元方法 - 更正规的方法是把所有需要用到的函数或模块在设置环境之前用 local 变量存储下来
第 3 种方法的示例:
local modname = "myComplex"
local M = {}
_G[modname] = M
package.loaded[modname] = M
local print = print
local string = string
setfenv(1, M)
function new(r, i)
return {r = r, i = i}
end
i = new(0, 1)
function add(c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end
function display(c)
print(string.format("(%d, %d)", c.r, c.i))
end
module 函数
module 中的代码都有类似的开头,Lua 5.1 提供了新函数 module 来囊括这些功能,示例:
-- 第二个参数的功能等价于setmetatable(M, {--index = _G})
module("myComplex", package.seeall)
function new(r, i)
return {r = r, i = i}
end
i = new(0, 1)
function add(c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end
function display(c)
print(string.format("(%d, %d)", c.r, c.i))
end
module 在创建模块 table 之前会先检查 package.loaded 是否已经包含该模块,如果找到了就直接复用该模块
子模块与包
一个 package 就是一个完整的模块树,它是 Lua 中发行的单位。层级关系用点来分隔,例如 mod.sub
就是 mod
的一个子模块
当 require 在 package.loaded 和 package.preload 中搜索的时候,直接用 mod.sub
作为 key 搜索,这个点没有任何特殊含义。但是在搜索一个定义子模块的文件的时候,require 会将点转换为另一个字符,通常就是目录分隔符。根据这个行为,可以把包中的所有模块组织到文件目录中,例如 mod/sub.lua
对 Lua 来说并没有目录的概念,同一个包中的子模块除了它们的环境 table 是嵌套的,它们之间没有显式的关联性
关于 require 加载 C 的子模块,详见 P142
16 面向对象编程
Person = {
age = 0,
-- 如果不用冒号,就要显式声明self变量
ageIncrement = function(self)
self.age = self.age + 1
end
}
-- 语法糖,冒号隐藏了self参数
function Person:ageDecrement()
self.age = self.age - 1
end
-- 冒号和点在定义和调用上可以随意使用,只要形式正确即可
Person:ageIncrement()
print(Person.age) -->1
Person.ageDecrement(Person)
print(Person.age) -->0
类
Account = {
balance = 0
}
function Account:deposit(v)
self.balance = self.balance + v
end
function Account:withdraw(v)
self.balance = self.balance - v
end
-- 使用继承,从原型生成实例
function Account:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
a = Account:new()
a:deposit(100.0)
--上面这句话的意思等价于:
--拆开语法糖
--a.deposit(a, 100.0)
--a自身没有deposit成员,从元表的__index中找
--getmetatable(a).__index.deposit(a, 100.0)
--通过继承,getmetatable(a).__index就是Account
--Account.deposit(a, 100.0)
--结果就是调用了原型中的函数,但是self参数是a
print(a.balance)
继承
-- Account
Account = {
balance = 0
}
function Account:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Account:deposit(v)
self.balance = self.balance + v
end
function Account:withdraw(v)
if v > self.balance then
error "insufficient funds"
end
self.balance = self.balance - v
end
function Account:printBalance()
print(self.balance)
end
-- SpecialAccount
SpecialAccount = Account:new()
-- 重定义了从基类继承的方法
function SpecialAccount:withdraw(v)
if v - self.balance >= self:getLimit() then
error "insufficient funds"
end
end
-- 编写新的方法
function SpecialAccount:getLimit()
return self.limit or 0
end
s = SpecialAccount:new{limit = 1000.0}
s:deposit(100.0) -- 对于找不到的成员,会一直沿着元表找到定义
s:printBalance()
-- 无需为指定一种新的行为而创建一个新类
-- 可以直接修改对象的行为
function s:getLimit()
return self.balance * 0.1
多重继承
local function search(k, plist)
for i = 1, #plist do
local v = plist[i][k]
if v then
return v
end
end
end
function createClass(...)
local c = {}
local parents = {...}
setmetatable(c, {__index = function(t, k)
return search(k, parents)
end})
c.__index = c
function c:new(o)
o = o or {}
setmetatable(o, c)
return o
end
return c
end
-- Account
Account = {
balance = 0
}
function Account:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Account:deposit(v)
self.balance = self.balance + v
end
function Account:withdraw(v)
if v > self.balance then
error "insufficient funds"
end
self.balance = self.balance - v
end
function Account:printBalance()
print(self.balance)
end
-- Named
Named = {}
function Named:getName()
return self.name
end
function Named:setName(n)
self.name = n
end
-- NamedAccount
NamedAccount = createClass(Account, Named)
account = NamedAccount:new{name = "Paul"}
print(account:getName())
可以把搜索到的基类成员复制到子类中来提升效率。但是运行后就很难修改方法的定义。
setmetatable(c, {__index = function(t, k)
local v = search(k, parents)
t[k] = v
return v
end})
私密性
P150:把私有和公有的成员放在两个 table 中,通过工厂函数来返回一个 closure
单一方法(single-method)做法
function newObject(value)
return function(action, v)
if action == "get" then
return value
elseif action == "set" then
value = v
else
error "invalid action"
end
end
end
d = newObject(0)
print(d("get")) -->0
d("set", 10)
print(d("get")) -->10
每个对象都是一个 closure,拥有完全的私密性控制,并且比 table 高效
17 弱引用 table
Lua 采用自动内存管理,GC(垃圾收集器)只能回收它认为是垃圾的东西,一些用户认为没用的东西并不会被释放,例如无用的全局变量,栈顶部被弹出的元素等等,此时往往需要用户主动将对象赋值为 nil
弱引用 table 是一种机制,用户用它来告诉 Lua 一个引用该不该阻碍一个对象的回收
通常情况下,table 中的 key 和 value 都是强引用,它们会阻止对其所引用对象的回收。有以下 3 种弱引用 table:
- 弱引用 key
- 弱引用 value
- 同时具有两种弱引用
a = {}
-- __mode字段决定一个table的弱引用类型
-- 如果包含'k',key就是弱引用的,包含'v',value就是弱引用的
mt = {__mode = "k"}
setmetatable(a, mt)
key = {}
a[key] = 1
key = {}
a[key] = 2
collectgarbage() -- 强制进行垃圾收集
for k, v in pairs(a) do
print(v) -->2
end
可以看到第一个 key 被回收了,因为垃圾收集的时候没有其他地方还在引用 key。而第 2 个 key 还存在别的引用,所以没有被回收
注意:
- 数字或者布尔这样的”值“是不可回收的。对于一个插入 table 的数字 key,收集器永远不会删除它,除非对应的 value 被回收了,那整个键值对就会从 table 中删除
- 可以简单地把字符串看成”值“,不会从弱引用 table 中被删除
备忘录(memoize)函数
提高函数运行速度的一种方法:在一个 table 中记录下函数计算的结果,下次再用同样的参数再次调用的时候,就可以复用之前的结果。可能带来的问题是有些参数只出现一次,导致 table 的大小膨胀,并且有很多无用的结果,此时可以使 table 具有弱引用的 value,这样每次垃圾收集都会删除所有在执行时未使用的编译结果
”备忘录技术“还能确保某类对象的唯一性,P155 以 RGB 颜色举例
对象属性
弱引用 table 的另一个应用,用于保存对象的属性,使用弱引用 key 的 table,将对象作为 key,对象的属性作为 value 即可。
回顾 table 的默认值
此处默认值的意思是,读取不存在的 index 的时候返回的值叫默认值
使用弱引用 table 把每个 table 与其默认值关联起来(其实类似于对象属性,无非这里的属性就是默认值)
local defaults = {}
setmetatable(defaults, {__mode = "k"})
local mt = {__index = function(t)
return defaults[t]
end}
function setDefault(t, d)
defaults[t] = d
setmetatable(t, mt)
end
或者对不同的默认值使用不同的元表也可以