chunk
Lua 里的 代码片段 被称为一个 chunk
顾名思义,代码块,可大可小,一个文件、一个方法、一行代码,都可以叫做 chunk
通常以一行作为一个块,不会写分号、不会用空格分隔
一个 dofile、require 加载的代码文件,其加载的内容也叫 chunk,例如报错的时候就会提示你 in xx chunk
使用 load 加载 chunk
load 方法是比较底层的,允许你加载一部分代码片段,
load (chunk [, chunkname [, mode [, env]]])
这些参数分别代表:
- chunk:代码片段
- chunkname:代码的识别名,在日志中会以这个名字打印
- mode:控制chunk的存在方式
- b: binary
- t: text
- bt: 两者均有
- env:传入 chunk 的参数
chunk 参数
chunk 在填入时 一定是一个 string,表示代码片段(就像例子中那样)
而整个 chunk 在被 load 编译成功时,会返回一个 可执行方法(为了便于叙述,我们叫它 compiler func
)
local loaded = load(chunk)
if loaded then
local result = loaded()
end
调用了这个返回的方法,才算是让这个 chunk 运行了(就像加载 module 那样)
chunk 片段内部(而不是 load 的结果 compiler func
) 必须返回一个值:
- 返回一个 string
- 或者 table、function,
如果 chunk 内部提前返回,则逻辑会在返回处截断 - 如果你没有返回,则默认返回 nil
例如这样一个例子,chunk 内部返回了一个字符串,当你执行 compiler func
时,chunk 被调用、返回值:
local loaded = load("return 'hello world'")
local str = loaded()
print(str)
而在这个例子中,chunk 内部没有返回值,你在调用 compiler func
后,只是执行了 chunk 内部的逻辑,不需要接收它的返回值:
local chunk = [[
globalVarInChunk = 123
]]
local loaded = load(chunk)
loaded()
print(globalVarInChunk)
而这种情况下, EmmyLua 等语法解释器是捕捉不到这么骚的操作的,他会认为你的代码实际上不存在这个全局变量,给你提出忠告
chunkname 参数
对于 chunkname 的理解可以参考这个例子,查看控制台输出:
local chunk = [[
local module = {}
function module:fun()
error("hello world")
end
return module
]]
local loaded = load(chunk, "Hello!World!", "bt")
local module = loaded()
module:fun()
env 参数
env 对应的是这个 chunk 的一个上下文环境,例如前面例子中使用了 print
函数,就是因为默认会把 _ENV
传进去给 chunk
方法的注释里会说这是用户提供的 访问参数(upvalues)
_ENV 和 _G 是类似的,可以参考:Environments and the Global Environment
而如果我们重写 load 的 env 参数为 {}
,会发现 print 函数不可用了,因为我们没有为它的环境变量提供对应的函数,
local chunk = [[
local module = {}
function module:fun()
print("hello world")
for k, v in pairs(_ENV) do
print(k, v)
end
end
return module
]]
local env = {}
env.print = print
env.pairs = pairs
local loaded = load(chunk, "test", "bt", env)
local module = loaded()
module:fun()
这种机制对于 上下文的继承关系,完全是由 load 的调用者决定的,换句话说每个 chunk 默认情况下都拥有独立的上下文。
但还是那句话,本质上只是在默认情况下共享了同一个全局上下文,
如果底层的编写者想这么做,他甚至可以将 env 中的一些方法进行改写,
至于要不要继承这个新的上下文,完全是新的 load 调用者决定的
local chunk1 = [[
print(globalVar)
globalVar = 456
local fakerPrint = function()
print("never gonna give you up~ ")
end
local env = {}
env.print = fakerPrint
return env
]]
local chunk2 = [[
print(globalVar)
]]
local env = {}
env.print = print
env.globalVar = 123
local loaded1 = load(chunk1, "test1", "bt", env)
env = loaded1()
local loaded2 = load(chunk2, "test2", "bt", env)
loaded2()
因为 lua 本身是各个国家的一些 民间组织、个人开发者 齐心协力搞的一个开源的、轻量的、易于注入的语言,这也导致它十分灵活,灵活过头了,
如果没有严格的 项目规范、约束,就很容易导致一些放飞自我的骚操作,让项目维护起来极其困难
dump
string 提供了 将 function 转化为 binary 字符串的方法
local dump = string.dump(function()
print("hello world")
end)
local loaded = load(dump)
loaded()
-> hello world
传参的话就是这样(绕一大圈回到原点)
local dump = string.dump(function(worlds)
print(worlds)
end)
local loadedPrint = load(dump)
loadedPrint("hello world")
loadfile
load file 和 load 很接近,区别在于它是直接从 file 加载内容,而不需要你指定字符串
require module
module 就是基于 load 再进行了一些常用封装,
也就是说 module 是 lua 抽象出来的概念,是模拟 OOD 的一种机制,并不是拥有什么特权
它由 require 和 package 两个功能联动实现
对于如何加载 module,有专门的四种 searchers,用来指示 require 如何加载一个外部的 lua chunk 代码:
- packaged.preloaded
- package.path:
- package.searchpath
- all-in-one loader
之后,require 会将文件加载、缓存到 package.loaded 表,然后每次被调用时返回表里缓存的内容
参考这篇文章进行 searcher 的定制: https://zhuanlan.zhihu.com/p/346355282
preload
预加载允许你定义一系列模板,在 require 的时候进行调用,
package.preload["test"] = function(module)
print(module)
end
local test = require("test")
正常一点的例子:
package.preload["some module"] = function(module)
local innerVar = 0
local module = {}
module.Call = function(self)
innerVar = innerVar + 1
print(innerVar)
end
return module
end
local module1 = require("some module")
module1:Call()
local module2 = require("some module")
module2:Call()
module2:Call()
和一般module一样,外部的局部变量,也是 module 内共享的,类似于 静态变量
而 preload 顾名思义也就是一种提前把需要的模块都加载进去、注册为一个 key,以便后续 require 的时候重定向的一种机制,
和常规意义上的提前加载不太一样
loaded
Programming in Lua : 8.1
require 的作用类似于 dofile 但是更面向模块
它会避免重复 require 一个 module 两次,会把加载文件缓存到 _loaded 目录
官方也提示如果确实有需要可以自己把 loaded 对于某个文件的索引置为 nil
Enviroment
在 load 中有 env 的概念,也就是上下文环境
而全局变量就是同一个 lua 虚拟机(这取决于你使用的框架)内部共享的 上下文环境(或者说环境变量)
_G
_G 表会持有所有全局环境的变量的引用,以及它自身的引用
value = "hello world"
print(_G["value"])
print(_G["_G"]["value"])
包括内置的一些方法都在 _G 表中,例如
_G.print("helloworld")
以至于支持这样的骚操作
function func1()
print("1")
end
function func2()
print("2")
end
function func3()
print("3")
end
for i=1, 3 do
_G["func"..i]()
end
并且文档里还介绍了一些更骚的操作,
但正常开发中使用这种操作会增加 理解难度、维护成本,尽量避免
_G 的元表
在 Lua 里定义、使用全局变量、方法,都可以视为访问 _G 表
因此我们可以像这样定义 _G 的 metatable 来帮助排错
setmetatable(_G, {
__newindex = function(_, n)
error("n>>> attempt to write to undeclared variable "..n, 2)
end,
__index = function(_, n)
error("n>>> atempt to read to undeclared variable "..n, 2)
end
})
hahaha()
->
>>> atempt to read to undeclared variable hahaha
对应的,如果想直接从 _G 里添加变量,可以借助 rawset 避免触发元方法
具体参考文档
_ENV
TODO