lua 中的代码片段

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

Programming in Lua : 14.1

在 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

Leave A Reply

Your email address will not be published. Required fields are marked *