Lua 和 LuaJIT#

Lua#

Lua 是一个开源项目,由巴西里约热内卢天主教大学的 Roberto Ierusalimschy、Luiz Henrique de Figueiredo 和 Waldemar Celes 创建。它的版本控制相对简单明了。

Lua 的版本体系#

Lua 的版本号通常是 X.Y 的形式,例如 5.1, 5.2, 5.3, 5.4

  • X (主版本号/Major Version): 表示一个重大更新,通常会引入不兼容的更改(breaking changes),新的核心特性,或者对虚拟机架构的显著改进。从 5.x5.yy 增加通常意味着语法、API 或语义的更改,可能需要修改现有代码。
    • 例子:
      • Lua 5.0: 引入了协程 (coroutines)。
      • Lua 5.1: 引入了模块系统 (module system)、vararg 参数的改进。
      • Lua 5.2: 引入了 goto 语句、环境 (environments) 的重新设计、新的 _ENV 上值。
      • Lua 5.3: 引入了整数类型、位操作 (bitwise operations)、UTF-8 支持。
      • Lua 5.4: 引入了新的垃圾回收器、弱表 (weak tables) 的改进。
  • Y (次版本号/Minor Version) 或补丁版本 (Patch Version): 在主版本中,通常用于表示错误修复、性能优化或次要的功能增强。这些更改通常是向后兼容的(backward compatible),不会破坏现有代码运行。有时候,一个版本号会是 X.Y.Z 的形式,Z 就代表补丁版本。
    • 例子: Lua 5.3.1, Lua 5.3.2, Lua 5.3.3 等等。这些通常是修复 bug。

LuaJIT#

https://github.com/LuaJIT/LuaJIT

LuaJIT 是 Lua 5.1 的一个高性能 JIT(Just-In-Time)编译器和运行时环境。它由 Mike Pall 开发,目标是提供与 C 语言接近的性能,同时保持 Lua 的简洁和灵活性。LuaJIT 的版本体系和开发模式比 Lua 更复杂一些,主要是因为其内部实现(JIT 编译器)的复杂性。

LuaJIT 的版本体系#

LuaJIT 的开发由 Mike Pall 驱动,2023年,LuaJIT 切换成 Rolling Releases 模式,因此其权威代码源是其 Git 仓库 git clone https://luajit.org/git/luajit.git。用户应定期从所选的 Git 分支拉取更新,以获取最新的修复和开发成果。

为了替代每次发布时手动递增版本号的做法,LuaJIT 的构建过程使用最新提交的 POSIX 时间戳(单调递增)作为语义版本中的发布号。完整的版本号格式为 $major.$minor.$timestamp,可以通过运行 luajit -v 命令来查看。

luajit -v 
LuaJIT 2.1.1756211046 -- Copyright (C) 2005-2025 Mike Pall. https://luajit.org/

LuaJIT 不提供发布版压缩包或二进制文件。请勿使用来自旧压缩包或 Zip 文件的过时版本。请删除任何指向这些下载的过期链接,因为它们将很快失效。

请勿使用第三方创建的伪发布版或压缩包。特别是,GitHub 和其他镜像自动生成的压缩包缺少 .git 目录,将无法再正确构建。

目前 LuaJIT 有两个主要的版本,各个分支的状态:

  1. v2.0 分支 (Legacy):

    • 2.0.x 系列现在仅推荐在需要保持兼容的场景下使用
    • 基于 Lua 5.1 语言规范。这意味着它支持 Lua 5.1 的所有特性,但不支持 Lua 5.2、5.3 或 5.4 中引入的新特性(如整数类型、位操作、新的 _ENV 行为等)。
    • 尽管它基于 Lua 5.1,但它也引入了一些非标准的扩展(例如 FFI - Foreign Function Interface),这是 Lua 5.1 不具备的。
  2. v2.1 分支 (Production):

    • 这是一个积极开发中的分支,用于引入新特性对后续 Lua 版本的支持
    • 它的目标是支持 Lua 5.2 和 Lua 5.3 的部分或全部新特性。这意味着它会引入整数类型、位操作等。
    • 注意: LuaJIT 2.1 旨在扩展对 Lua 5.2/5.3 特性的支持,但它不会成为一个完整的 Lua 5.2/5.3 实现。它将现有 Lua 5.1 的基础上,逐步添加这些新特性。例如,它不会复制 _ENV 的所有新行为,或者 5.4 的新 GC。
    • 最近的更新表明,Mike Pall 正在努力使 2.1 尽可能接近 Lua 5.2/5.3 规范。
Branch Maintained Breaking Changes New Features Recommended Use
v2.0 bugfixes no no Compatibility only
v2.1 yes no limited Production
(TBA) yes yes yes Development

OpenResty LuaJIT#

https://github.com/openresty/luajit2

OpenResty LuaJIT 是 OpenResty 维护的 LuaJIT 的一个下游分支,主要用于 Nginx Lua Module。相比于上游 LuaJIT,OR 的 LuaJIT 主要进行了一些针对性的性能优化和 APIs 的开发。

核心差异#

特征 Lua 5.x LuaJIT 2.0.x LuaJIT 2.1.x
语言规范 官方最新 Lua 规范 (5.4) Lua 5.1 规范 目标是支持 Lua 5.2/5.3 部分特性
性能 解释执行 (VM 性能随版本提升) JIT 编译,通常比 Lua 快数倍到数十倍 JIT 编译,性能预计与 2.0.x 相当,或略有波动
新特性 整数、位操作、UTF-8 (5.3+) 等 不支持 Lua 5.2+ 的新特性 逐步支持 Lua 5.2/5.3 的新特性
FFI 无内置 FFI 内置成熟的 FFI (Foreign Function Interface) 内置成熟的 FFI
GC (垃圾回收) 更先进的 GC (5.2+), 5.4 重写 基于 Lua 5.1 的 GC,调优 基于 Lua 5.1 的 GC,调优
生态系统 广泛支持,模块通常兼容最新 Lua 版本 C 模块通常需要绑定到 Lua 5.1 或使用 FFI 旨在更好地兼容 Lua 5.2/5.3 模块
维护者 Lua.org 项目组 Mike Pall (一人维护为主) Mike Pall (一人维护为主)

Benchmark#

http://software.rochus-keller.ch/are-we-fast-yet_LuaJIT_2017_vs_2023_results.pdf

651670eb2bf9c6f469b6c03d7664542b_MD5

基本语法#

https://learnxinyminutes.com/lua/

命名规范#

Lua 没有严格的官方规范,但 snake_case 是最主导且推荐的命名风格,尤其适用于局部变量、函数和模块名。对于常量,通常使用 UPPER_SNAKE_CASE。在模拟类时,PascalCase 用于类名或构造函数名。


-- snake_case (推荐)
local user_name = "Alice"
local total_count = 100
local is_ready = true

local function calculate_sum(a, b)
    return a + b
end

function get_user_data(user_id)
    -- ...
end

-- camelCase (如果项目或框架约定如此,也无妨,但要保持一致)
-- local function calculateSum(a, b)
--     return a + b
-- end
-- function getUserData(userId)
--     -- ...
-- end


-- 常量
local MAX_CONNECTIONS = 100
local PI = 3.14159
local DEFAULT_TIMEOUT = 5000 -- milliseconds

-- 模拟类
local Account = {}
Account.__index = Account -- 用于元表继承

function Account:new(initial_balance)
    local o = { balance = initial_balance or 0 }
    setmetatable(o, self)
    return o
end

function Account:deposit(amount)
    self.balance = self.balance + amount
end

local my_account = Account:new(100)


-- 特殊前缀 is_/has_/can:用于布尔型变量和函数
local is_active = true
local has_permission = false
function can_access(user_id) ... end


-- _:用于标记内部或私有的变量或函数,表示不应由外部直接访问。
local _internal_counter = 0

local function _do_private_work()
    _internal_counter = _internal_counter + 1
    print("Private work done.")
end

function public_api_call()
    _do_private_work()
    -- ...
end

-- M:代表 Module,用于收集并返回模块的公共 API
-- my_utils.lua
local M = {}

function M.add(a, b)
    return a + b
end

function M.subtract(a, b)
    return a - b
end

return M

类型系统#

Lua 是一种动态类型语言,其类型系统相对简单但功能强大。它不强制变量在编译时声明类型,而是在运行时根据赋值自动确定变量的类型。

以下是 Lua 中主要的类型:

  1. nil (空)

    • nil 是 Lua 中无效值或不存在值的唯一类型。
    • 任何未赋值的全局变量或局部变量在首次使用时都默认为 nil
    • nil 赋值给一个变量会使其无效化,类似于删除该变量。
    • 在条件判断中,只有 falsenil 被认为是假 (falsy),其他所有值都被认为是真 (truthy)。
  2. boolean (布尔)

    • 有两种布尔值:truefalse
    • 主要用于控制流语句(如 if, while)。
    • 所有关系运算符(例如 \==, ~=, <, >) 的结果都是布尔值。
  3. number (数字)

    • Lua 5.3 引入了整数和浮点数两种子类型,但在更早的版本中,所有数字都是双精度浮点数。
    • 通常,Lua 使用双精度浮点数表示所有数字,这足以满足大部分需求。
    • 整数的表示形式:1, 100, -5
    • 浮点数的表示形式:3.14, 0.5, 1e3 (1000.0)
  4. string (字符串)

    • 由零个或多个字符组成的序列。
    • 字符串是不可变的:一旦创建,就不能改变它的内容。字符串操作(如连接)总是创建新的字符串。
    • 可以用单引号 (')、双引号 (") 或双层方括号 ([[ ]]) 来定义。
    • 双层方括号可以定义多行字符串,并且不会解释转义序列(除非在开头添加 \=)。
    • 例如:"hello", 'world', [[A multi-line. string.]]
  5. table (表)

    • Lua 中最重要也是最强大的数据结构,是一种关联数组。
    • 可以用来模拟数组、哈希表(字典)、对象、结构体等。
    • 表是对象,通过引用传递。
    • 键(key)可以是除了 nilNaN 之外的任何 Lua 值(number, string, boolean, table, function, userdata, thread)。
    • 值(value)可以是任何 Lua 值(包括 nil,但将 nil 作为值赋值给表键意味着删除该键值对)。
    • 当用整数作为键时,可以像数组一样使用。数组索引通常从 1 开始(约定俗成,但也可以从其他值开始)。
    • 创建方式:{}
    • 例如:
      • t = {}
      • t.name = "Lua" (等同于 t["name"] = "Lua")
      • t[1] = 10
      • t["key"] = "value"
  6. function (函数)

    • 函数是“第一类公民”,可以像其他值一样被存储在变量中、作为参数传递、作为返回值返回。
    • Lua 支持闭包(closures):函数可以访问并操作其外部作用域中的变量。
    • 例如:
      • function greet(name) print("Hello, " .. name) end
      • my_func = function(x, y) return x + y end
  7. userdata (用户数据)

    • userdata 允许 Lua 包装任意的 C 数据结构。
    • 有两种类型:
      • Full userdata (完整用户数据):是 Lua 管理的内存块,其大小由创建时指定。它可以有关联的元表。
      • Light userdata (轻量用户数据):只是一个 C 指针(void*)。它没有元表(但可以有相关的元表,通过其指向的 C 数据来间接获取),也不由 Lua 的垃圾回收器管理。
    • 主要用于与 C/C++ 代码交互,扩展 Lua 的功能。
  8. thread (线程)

    • 代表一个独立的执行线程,用于协程(coroutine)。
    • 协程是非抢占式多任务处理的一种形式,它们允许函数暂停执行并在稍后恢复。
    • 通过 coroutine.create() 创建。

Lua 类型系统的几个关键点:

  • 动态类型 (Dynamic Typing): 变量没有固定的类型。它的类型取决于它当前存储的值。例如:
    local x = 10         -- x 是一个 number
    x = "hello"          -- 现在 x 是一个 string
    x = { a = 1 }        -- 现在 x 是一个 table
    
  • 强类型 (Strong Typing): Lua 是一种强类型语言,意味着你不能对不同类型的值执行不明确的操作。例如,你不能直接将一个字符串与一个数字相加(除非字符串可以被隐式转换为数字,但这通常不是自动的,或者你需要明确转换)。
    print("10" + 5) -- 这种操作在 Lua 中是错误的,会导致运行时错误
                     -- (attempt to perform arithmetic on a string value)
    
    你需要显式转换:print(tonumber("10") + 5)
  • 垃圾回收 (Garbage Collection): Lua 使用自动垃圾回收机制来管理内存,程序员不需要手动释放内存。nil 值对于垃圾回收来说很重要,将变量设置为 nil 会帮助垃圾回收器确定该值不再被引用。
  • type() 函数: Lua 提供了一个内置函数 type() 来检查任何值的类型,它返回一个字符串,例如 "nil", "number", "string", "boolean", "table", "function", "userdata", "thread"

编程范式#

Functional Programming#

Lua 不是一个纯粹的函数式语言(它也支持面向对象和命令式编程),但是它的一些语言特性可以用来实践函数式编程。

1. 一等公民函数(First-Class Functions)#

这是函数式编程最核心的特性之一,Lua 对此有完备的支持:

  • 函数可以赋值给变量: 你可以将一个函数视为一个普通的值,将其赋予一个变量。
    local add = function(a, b)
        return a + b
    end
    
    print(add(2, 3)) -- 输出 5
    
  • 函数可以作为参数传递给其他函数: 这使得高阶函数成为可能。
    local function apply(func, x, y)
        return func(x, y)
    end
    
    local function multiply(a, b)
        return a * b
    end
    
    print(apply(multiply, 4, 5)) -- 输出 20
    
  • 函数可以从其他函数返回: 这也支持高阶函数和部分应用/柯里化。
    local function make_adder(x)
        return function(y)
            return x + y
        end
    end
    
    local add_five = make_adder(5)
    print(add_five(10)) -- 输出 15
    
  • 函数可以存储在数据结构中: 如表(table)。
    local operations = {
        add = function(a, b) return a + b end,
        sub = function(a, b) return a - b end
    }
    
    print(operations.add(10, 2)) -- 输出 12
    

2. 匿名函数 (Anonymous Functions / Lambdas)#

Lua 使用 function 关键字后不跟函数名来创建匿名函数。这在作为参数传递给高阶函数时非常常见。

local numbers = {1, 2, 3, 4, 5}

-- 使用匿名函数过滤偶数
local even_numbers = {}
for i, v in ipairs(numbers) do
    if (function(n) return n % 2 == 0 end)(v) then
        table.insert(even_numbers, v)
    end
end
print(table.concat(even_numbers, ", ")) -- 输出 2, 4

3. 闭包 (Closures)#

当一个内部函数引用了其外部函数的局部变量,即使外部函数已经执行完毕,这个内部函数(及其引用的变量)仍然存在,这就是闭包。Lua 对闭包有强大的支持,这是实现高阶函数、状态封装和柯里化的关键。

local function make_counter()
    local count = 0 -- 外部函数的局部变量
    return function() -- 内部函数形成闭包
        count = count + 1
        return count
    end
end

local counter1 = make_counter()
local counter2 = make_counter()

print(counter1()) -- 输出 1
print(counter1()) -- 输出 2
print(counter2()) -- 输出 1 (独立的计数器)

4. 高阶函数 (Higher-Order Functions)#

由于函数是第一公民,Lua 可以轻松实现高阶函数,即接受一个或多个函数作为参数,或者返回一个函数的函数。常见的函数式编程模式如 mapfilterreduce(或 fold)都可以很容易地在 Lua 中实现。

示例:map

local function map(func, list)
    local result = {}
    for i, v in ipairs(list) do
        result[i] = func(v)
    end
    return result
end

local numbers = {1, 2, 3, 4, 5}
local squared_numbers = map(function(n) return n * n end, numbers)
print(table.concat(squared_numbers, ", ")) -- 输出 1, 4, 9, 16, 25

示例:filter

local function filter(predicate, list)
    local result = {}
    for _, v in ipairs(list) do
        if predicate(v) then
            table.insert(result, v)
        end
    end
    return result
end

local numbers = {1, 2, 3, 4, 5, 6}
local evens = filter(function(n) return n % 2 == 0 end, numbers)
print(table.concat(evens, ", ")) -- 输出 2, 4, 6

5. 表作为数据结构 (Tables as Data Structures)#

Lua 的表是其唯一且极其灵活的数据结构。它们可以用于模拟列表、字典、集合,甚至对象。在函数式编程中,数据通常是不可变的,并且通过函数操作来转换数据。虽然 Lua 的表本身是可变的,但你可以通过约定或编写辅助函数来模拟不可变性(例如,每次操作都返回一个新表)。

6. 多返回值 (Multiple Return Values)#

Lua 函数可以返回多个值,这在某些场景下可以使代码更简洁,例如返回操作结果和状态。虽然不是直接的函数式编程特性,但它有助于更灵活的数据流处理。

local function divide(a, b)
    if b == 0 then
        return nil, "division by zero"
    else
        return a / b
    end
end

local result, err = divide(10, 2)
if err then
    print("Error:", err)
else
    print("Result:", result) -- 输出 Result: 5
end

7. 对尾调用优化的支持 (Tail Call Optimization - TCO)#

Lua 完整地支持尾调用优化。这意味着一个函数在尾部调用另一个函数时,当前的栈帧会被重用而不是创建新的栈帧,从而避免栈溢出。这使得在函数式编程中常见的递归算法(尤其是尾递归)可以安全高效地使用,而无需担心性能或栈深度限制。

-- 尾递归实现阶乘
local function factorial_tail(n, acc)
    acc = acc or 1
    if n == 0 then
        return acc
    else
        return factorial_tail(n - 1, acc * n) -- 尾调用
    end
end

print(factorial_tail(5)) -- 输出 120
-- print(factorial_tail(100000)) -- 大数也可以计算,不会栈溢出

Everything is a table#

Everything is a table 这句话之所以成为 Lua 的标志,是因为在绝大多数情况下,我们在 Lua 中所使用的、抽象的、复合的、需要组织的数据结构,都是通过表来实现的。

1. 数据结构#

  • 数组 (Array): Lua 没有原生数组类型。数字索引的表完美地充当了数组的角色。
    local myArray = {10, 20, 30}
    print(myArray[1]) -- 10
    
  • 哈希表/字典/关联数组 (Hash Table/Dictionary/Associative Array): 表的核心功能。
    local myDictionary = {name = "Alice", age = 30, city = "New York"}
    print(myDictionary.name) -- Alice
    print(myDictionary["age"]) -- 30
    
  • 记录/结构 (Record/Struct): 同样是键值对的表。
    local user = {id = 123, username = "john_doe", email = "john@example.com"}
    
  • 堆栈/队列/链表 (Stack/Queue/Linked List): 可以通过表的insertremove等方法或手动实现。
    local stack = {}
    table.insert(stack, 10) -- push
    local item = table.remove(stack) -- pop
    
  • 集合 (Set): 可以通过表将元素作为键,值设为true来实现。
    local mySet = {apple = true, banana = true}
    if mySet.apple then print("apple is in set") end
    

2. 对象系统和面向对象编程 (OOP)#

Lua 是一种多范式语言,但它没有原生类(Class)的概念。OOP 在 Lua 中完全通过表和元表(Metatable)实现:

  • 对象 (Object): 每个对象都是一个表,存储其状态(属性)。
    local myObject = {x = 10, y = 20}
    
  • 类 (Class) / 原型 (Prototype): 通常也是一个表,包含共享的方法和初始化逻辑。
    -- 这是一个简单的“类”表,作为原型
    local Class = {}
    Class.__index = Class -- 用于实现继承查找方法
    
    function Class:new(x, y)
        local obj = setmetatable({}, self) -- 创建新表,设置元表
        obj.x = x
        obj.y = y
        return obj
    end
    
    function Class:move(dx, dy)
        self.x = self.x + dx
        self.y = self.y + dy
    end
    
    local p1 = Class:new(10, 20)
    p1:move(1, 1)
    print(p1.x, p1.y) -- 11, 21
    

在这个例子中,Class本身是一个表,p1也是一个表。元表__index也指向一个表。

3. 模块 (Module)#

Lua 模块通常也是一个表,将所有需要导出的函数和变量存储在其中,并通过return返回。

-- mymodule.lua
local M = {} -- M 就是一个表

function M.sayHello(name)
    print("Hello, " .. name)
end

function M.add(a, b)
    return a + b
end

return M -- 返回这个表
-- main.lua
local myModule = require("mymodule")
myModule.sayHello("World")
print(myModule.add(1, 2))

4. 环境 (Environment)#

  • 全局环境 (_G): 这是一个表,包含了所有全局变量、函数和标准库。
    print(_G.print) -- function: ...
    print(_G.string) -- table: ... (string库本身也是一个表)
    
  • 自定义环境: 可以通过setfenv(旧版)或 _ENV(新版)为函数或线程设置独立的表作为其环境,从而实现沙箱隔离或模块化。

5. 元表 (Metatables)#

元表本身就是表。它定义了宿主表的行为。

local myTable = {}
local myMetatable = {
    __add = function(a, b) return a.value + b.value end
}
setmetatable(myTable, myMetatable)

这里,myMetatable就是一个表。


除了上述基于表的数据结构,Lua 本身的基本(原子)数据类型和函数。它们不是表,但它们可以作为表的

  • nil: 既不是表,也不能作为表的键(但可以作为表的值,来“删除”一个键)。
  • boolean: truefalse,不是表。
  • number: 不是表。
  • string: 不是表。
  • function: 函数是 Lua 中的一等公民,它们本身有自己的类型 (function),而不是表。虽然函数可以存储在表中,作为表的值,但函数本身不是表。
    local func = function() print("hello") end
    print(type(func)) -- function
    
  • userdata: 通常是 C 语言层面的原始内存块或对象指针的封装,提供给 Lua 使用。它们不是表。
  • thread: Lua 协程对象也不是表。

而这些基本数据类型不是表的主要原因也是为了效率和内存优化

  • 原始值的存储: 数字、布尔值、nil 都可以直接以字面值的形式存储,或者在堆栈上直接操作。如果它们都是表,就意味着每次使用这些基本值时,都需要分配一个表结构(即使是空表),这会带来巨大的内存开销和性能损失。
  • 不变性: 数字、字符串等是不可变的。表是可变的。将不可变的数据封装在可变的数据结构中,会增加不必要的复杂性。
  • 唯一性: niltruefalse在 Lua 中是单例的。如果它们是表,每次引用都需要确保引用的是同一个表实例,这又是额外的开销。
  • C 语言的桥接: userdatafunction(C closure)是为了与 C 语言进行高效互操作而设计的。它们直接对应 C 语言的指针或函数指针,如果强行包装成表,会打破这种直接映射,增加不必要的中间层。

虽然这些类型本身不是表,但它们与表有着紧密的联系:

  • 作为表的键: 除了 nil,所有其他基本类型(包括 boolean,虽然不常见,但合法)都可以作为表的键。
    local t = {}
    t[1] = "number key"
    t["hello"] = "string key"
    t[true] = "boolean key"
    local myFunc = function() end
    t[myFunc] = "function key"
    local co = coroutine.create(function() end)
    t[co] = "thread key"
    
  • 作为表的值: 所有基本类型都可以作为表的值。这是最常见的用法。
    local data = {
        name = "Lua",
        version = 5.4,
        isActive = true,
        greet = function() print("Hi!") end,
        config = nil -- 表示配置项缺失或已删除
    }
    

Module System#

Lua 的模块系统是其语言设计中非常优雅和实用的一个部分。它没有像 Python 或 Java 那样复杂的关键字或机制,而是高度依赖于表的概念和 require 函数。这种设计哲学使得 Lua 的模块系统既简单又强大。

核心概念与工作原理#

  1. 模块即表(Table is Module): 上面介绍表时也提到过,Table 是 Lua 模块系统的基石。一个 Lua 模块实际上就是一个普通的 Lua table。这个 table 包含了模块对外暴露的所有函数、变量(或称为“成员”)。

  2. require 函数:加载与缓存: 这是加载和使用模块的核心函数。

    • 查找路径:当你调用 require("mymodule") 时,Lua 会按照一个预定义的路径(package.path 字符串)去查找名为 mymodule 的文件。
      • 它会尝试在 package.path 中的每一个路径模式中替换 ?mymodule,并尝试加载这个文件。
      • 通常,它会先找 mymodule.lua,然后是编译过的 C 模块 (.so.dll)。
    • 加载
      • 如果找到一个 Lua 文件,require 会加载并执行这个文件。
      • 模块文件通常会返回一个 table,这个 table 就是模块本身。
    • 缓存require 函数有一个非常重要的特性:它会缓存已加载的模块。一旦一个模块被 require 加载过,它就会被存储在 package.loaded 表中。下次再调用 require 加载同一个模块时,require 不会重新执行模块文件,而是直接从 package.loaded 表中返回缓存的版本。
      • 这确保了模块初始化只发生一次,并避免了重复加载的开销。
      • 这意味着模块文件中的顶层代码(不属于任何函数)只会在第一次 require 时执行。
  3. 返回模块表: 一个标准的 Lua 模块文件(例如 mymodule.lua)的最后一行通常会 return 一个 table。这个 table 就是该模块对外提供的接口。

模块的创建(编写一个模块)#

假设我们要创建一个名为 math_utils.lua 的模块,它提供一些额外的数学函数。

-- math_utils.lua

-- 1. 创建一个局部表来表示模块
local M = {} -- M 是 Module 的缩写,这是Lua社区的常见约定

-- 2. 在这个表中定义模块的函数和变量
function M.add(a, b)
    return a + b
end

function M.subtract(a, b)
    return a - b
end

-- 可以定义私有函数和变量(不放入 M 表中)
local function multiply_internal(a, b)
    return a * b
end

function M.multiply(a, b)
    -- 模块对外暴露的函数可以调用私有函数
    return multiply_internal(a, b)
end

M.PI = 3.14159 -- 暴露一个常量

-- 模块的顶层代码,只在第一次require时执行
print("math_utils module loaded!")

-- 3. 返回这个模块表
return M

关键点:

  • 使用 local M = {}:创建一个局部表,避免全局污染。这也是 Lua 模块的最佳实践。
  • 将函数和变量作为 M 的字段:M.add = function(...)function M.add(...)
  • 使用 local function 定义私有函数:这些函数不会被 return M 导出,只能在模块内部使用。

模块的使用(加载和调用)#

现在,我们可以在另一个 Lua 脚本中使用 math_utils 模块了。

-- main.lua

-- 1. 加载模块
local math_utils = require("math_utils") -- require 返回模块表

-- 2. 调用模块的函数和访问模块的变量
print(math_utils.add(10, 5))          -- 输出: 15
print(math_utils.subtract(10, 5))     -- 输出: 5
print(math_utils.multiply(3, 4))      -- 输出: 12
print(math_utils.PI)                  -- 输出: 3.14159

-- 第一次 require 会打印 "math_utils module loaded!"
-- 第二次 require 不会再次打印,因为模块已被缓存
local another_ref = require("math_utils")
print(another_ref.add(1, 1))           -- 输出: 2
print(another_ref == math_utils)       -- 输出: true (指向同一个表)

当运行 main.lua 时:

  1. require("math_utils") 会查找 math_utils.lua
  2. 执行 math_utils.lua 文件。
  3. 打印 “math_utils module loaded!"。
  4. math_utils.lua 返回 M 表。
  5. math_utils 变量在 main.lua 中现在引用了这个模块表。
  6. 后续对 require("math_utils") 的调用将直接从 package.loaded 中获取缓存的表,不会重新执行 math_utils.lua

模块查找路径 (package.pathpackage.cpath)#

require 函数查找模块的路径由 package.pathpackage.cpath 两个全局变量决定:

  • package.path:用于查找 Lua 模块文件 (.lua)。它是一个由分号分隔的路径模式字符串。
    • 例如:./?.lua;./sublibs/?.lua;/usr/local/lua/?.lua
    • ? 是一个占位符,require 函数会用模块名替换它。
  • package.cpath:用于查找 C 语言编译的模块文件 (.so.dll)。它也有类似的路径模式。

你可以通过修改这些变量来添加自定义的模块查找路径:

-- 在你的入口文件(main.lua)的开头
package.path = package.path .. ";./my_custom_modules/?.lua"
-- 或
package.path = "./my_custom_modules/?.lua;" .. package.path

兼容性处理 (module 函数 - Lua 5.1 旧有机制)#

在 Lua 5.1 中,有一个内置的 module 函数被用来简化模块创建。它的用法如下:

-- mymodule_51.lua (Lua 5.1)
module(..., package.seeall) -- ... 是当前模块名

-- 所有后续的全局函数和变量都会被自动放入模块表中
function add(a, b)
    return a + b
end

PI = 3.14

然而,module 函数:

  1. 会污染全局环境:它会修改当前的全局环境,不是一个好的实践。
  2. 已被废弃:在 Lua 5.2 及更高版本中,module 函数已被移除。

因此,强烈建议使用 local M = {} 然后 return M 的标准模式来创建模块。

最佳实践#

  1. 使用 local M = {}return M:这是现代 Lua 开发的标准和推荐方式。它避免了全局污染,清晰地定义了模块的接口。

  2. 将模块文件放在 package.path 或其子路径中:方便 require 自动找到。

  3. 使用 local 定义模块内部私有函数和变量:保持模块的封装性,避免不必要的暴露。

  4. 模块文件末尾 return 语句必不可少:这是 require 函数获取模块表的机制。

  5. 避免在模块中产生不必要的全局变量:除了 return 的模块表,模块内部的代码应该尽可能地使用 local 变量。

  6. 善用 package.loaded:如果你需要强制重新加载一个模块(通常不推荐,只在特殊调试场景下),可以删除 package.loaded["mymodule"]

    -- 强制重新加载模块(不推荐,除非你知道你在做什么)
    package.loaded["math_utils"] = nil
    local fresh_math_utils = require("math_utils")