笔记 - Lua 程序设计(第 2 版) Ch14~17

Lua中的全局变量存储在环境table中,可通过全局变量_G访问。本文详细介绍了如何遍历全局变量,动态获取和设置变量,以及如何限制对未声明全局变量的访问。此外,还探讨了非全局环境的创建,特别是通过setfenv改变函数环境。最后,讨论了模块与包的管理,包括require函数的工作原理和自定义模块的实现。文章还涉及面向对象编程,如类和继承,以及弱引用table的应用,如创建私有变量和优化内存管理。
摘要由CSDN通过智能技术生成

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
  1. package.loaded 中检查模块是否已被加载,如果是,直接返回相应的值(避免无限循环)
  2. 如果还没被加载,先尝试找一个加载器,首先在 package.preload 中查找,大概率不会找到,接着尝试从 Lua 文件或 C 程序库中加载模块,分别通过 loadfile 或者 loadlib 来加载(注意这里只是加载,还没有运行)。如果找不到加载器,报错
  3. package.loaded 中标记模块已加载,避免无限循环
  4. 运行代码,如果有返回值,丢入 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

或者对不同的默认值使用不同的元表也可以

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值