Lua 速查手册

22 分钟阅读

Lua 是一门很精简的语言。从语法结构与涉及的概念上来说,体量都非常小,很快就能学会; 同时,作为一门程序语言又有足够的表达能力。因此,我认为很适合用于代替伪代码,用于 展示程序语言的概念。

这篇小手册旨在让对 Lua 完全不熟悉,但是已经在其它程序语言中掌握了编程基本概念的 读者,能通过本文快速对 Lua 有一定的了解。

基本结构

脚本整体是一个执行区块,可以看做是一个无参数函数的函数体。脚本被调用时,语句会 从上向下依序执行。

语句可以使用分号做结,也可以不用。当想在一行中书写多个语句时,就需要使用分号分割 语句。

-- 间行注释
--[[
多行注释
]]

function foo()
    return "world"
end

print("hello", foo())

变量

所有的变量默认都是全局的,只有使用 local 关键字修饰的变量名的访问会被限制在当前 的作用域里。

nil 是一个特殊的变量值,用于表示值不存在。如果一个变量只进行了声明却没有进行 赋值,变量的值就是 nil

-- 变量首次被赋值时变量名会被创建
a = 1 -- 全局变量
local b = 2 -- 局部变量

-- 以下两种形式创建的变量对应的值都是 nil
local c
local d = nil

变量类型

type() 函数以字符串的形式返回变量的类型,可以用于进行变量的类型判断,比如:

if type(foo) == "string" then
    -- ...
end

Lua 的原生变量类型很少,这里介绍以下几种:

  • number

    Lua 中的数字类型不区分整数与浮点数,不像在其它语言中整数和浮点数是不同的二进制 形式。

    在新版本的 Lua 中,解释器会尽可能区分整数与浮点数,用原生的整数格式进行整数的 储存。

  • boolean

    布尔常量是 truefalse

    在 Lua 中,条件语句里是不会对变量进行自动的布尔化的。被认定为假值的只有 falsenil。其它所有值都是真,包括 0、空字符串。

  • string

    使用单引号、双引号都可以定义单行字符串字面量。多行字符串使用 [[]] 定义, 比如:

    local msg = [[hello
    world]]
    

    同时,多行字符串中不会进行字符串的转义。

    Lua 的字符串是一个 8-bit 整数数组,不预定义任何编码。因此也可以用于储存二进制 数据流。

表(table)

表是两种不同的数据结构的组合:数组、字典(哈希表)。

数组也可以看做是以数字作为键的字典。Lua 的解释器会自行根据传入的索引值的类型, 分别进行表中数组部分与字典部分的区分索引。

正是由于数组与表的一体化,在 Lua 中没有专门以 0 索引首个数组元素的必要了, Lua 中数组的索引是以 1 起头的

其它语言中的结构体、类这样的概念亦是通过表来实现的。使用属性名进行字段值的索引, 形式是与使用字符串值进行字典值查询一样的。

在 Lua 中,除了 nil 之外的所有值都可以用作表键。

a = {}

a[1] = 1      -- 增加新的元素
print(a[1])

-- 查询表的长度,会检查从索引 1 开始连续有效的索引的个数。
print(len(a))
print(#a)

-- 添加使用字符串键索引的值
a["a"] = 1
a.b = 2

-- 键的类型是字符串时,以下两种索引方式是等价的
print(a.a, a["a"])

一个键对应的值是 nil 与表中没有该键是等价的。如果用某一键进行表索引,得到的值 是 nil 时,就表示表中没有该键。

-- 进行表中已有的键值对的删除
a = {}
a[1] = 1

a[1] = nil

表中的键值对可以用两种形式进行遍历:

a = {}
a[1] = 1

-- 遍历从 1 开始所有有效的连续索引
for index, value in ipairs(a) do
    -- ...
end

-- 遍历表中所有的键值对,顺序不确定
for key, value in pairs(a) do
    -- ...
end

控制流

  • 条件语句

    if cond1 then
      -- ...
    elseif cond2 then
      -- ...
    else
      -- ...
    end
    
  • 逻辑运算

    a = true
    b = false
    print(a and b, a or b, not a)
    

    andor 都支持运算短路。并且两种表达式的值都是短路发生时所在位置的表达式 的值(注意,该值不一定是布尔类型的)。

    具体来说,如果 a 的值既不是 nil 也不是 false,以下两种写法是等效的:

    local result
    if cond then
      result = a
    else
      result = b
    end
    
    local result = cond and a or b
    
  • 循环

    -- 输出 1 到 10
    for i = 1, 10 do
        print(i)
    end
    
    -- 输出 10 到 1
    for i = 10, 1, -1 do
        print(i)
    end
    
    -- 输出 1 到 10
    local i = 1
    while i <= 10 do
        print(i)
        i = i + 1
    end
    
    -- 输出 1 到 10
    local i = 1
    repeat
        print(i)
        i = i + 1
    until i > 10
    
  • 语句块

    可以用于创建变量的作用域,限制变量名的可访问范围。

    local fib
    do
        local old, inc = 0, 1
        fib = function()
            local new = old + inc
            old, inc = inc, new
            return old
        end
    end
    
    for i = 1, 10 do
        print(fib())
    end
    

函数

定义函数有两种等价的方式:

function foo(a, b, c, ...)
    -- ...
end

foo = function(a, b, c, ...)
end

在调用函数时,如果函数只有一个参数,且这个参数类型是字符串或者表,那么可以省略 函数调用的括号:

print "hello world"
table.join { "hello", "world" }

函数调用时,如果实际传入的参数数量少于函数定义时的参数数量,那么没有被传入的参数 在函数体执行时就是 nil 值。

... 是一个特殊的变量名,只能用在参数表的最后,用于表示任意非负数量的参数。 在函数体中需要转换为表来进行访问:

function print(...)
    for i, v in ipairs { ... } do
        if i > 1 then
            io.write "\t"
        end
        io.write(v)
    end
end

函数使用 return 语句结束函数体的执行并指定返回值。函数的 return 语句中没有 指定值的情况与函数体中不使用 return 语句自然结束执行一样,函数都会给出返回值 nil

函数也是变量的值,可以储存到变量中、可以作为其它函数的参数使用:

function foo(a, b, after)
    local result = a + b
    after(result)
end

foo(1, 2, print)

Lua 的函数是闭包,也就是说函数在被调用时,其函数体内可以访问该函数被定义时上下文 中已经存在的变量名:

function make_adder(n)
    return function(x)
        return x + n
    end
end

local adder = make_adder(2)
print(adder(1)) -- output: 3

面向对象编程

Lua 中唯一的数据结构是表,面向对象也是通过表实现的。

元表

Lua 通过元表(metatable)来对一个表变量进行额外的说明,比如:

  • 当使用该表中没有的键进行值查询时,应该返回什么值。
  • 当使用该表中没有的键进行键值对赋值时,应该进行什么操作。
  • 当该表与其它表进行四则运算时,应该如何响应。
  • ……

任何一个表都可以被用作元表(包括作为自身的元表),而表操作的每一个功能都对应一个 元表的键。

local meta = {}
local tbl = {}
setmetatable(tbl, meta) -- 将 meta 设为 tbl 的元表,该函数的返回值也是 tbl

print(getmetatable(tbl) == meta)

这里介绍元表键 __index,该键对应“使用表中没有的键进行值查询时,应该返回什么值” 的行为指定。

__index 键对应的值可以是表,也可以是一个函数。

  • __index 的值是表时,表示使用该表中的查询值作为被元表修饰的表中不存在的键 的值。
  • __index 的值是函数时,在元表所修饰的表中查询不存在的键值对时,会调用 __index 给出的函数使用其返回值作为查询结果,调用时函数的参数是元表修饰的表、需要查询的键。
local meta = {
    __index = { foo = 1 }
}
local tbl = setmetatable({}, meta)

print(tbl.foo) -- output: 1

表键的 : 访问

表中某个键对应的值是函数时,访问该值并调用的语法和其它语言中调用对象方法的形式是 一样的:

local foo = {
    bar = function() print("hello") end
}

foo["bar"]()
foo.bar()

如果使用 : 代替 . 进行字段值的访问,则表示使用作为被索引对象的表作为函数调用 的首个参数,占用一个参数位。

local foo = {
    bar = function(tbl) print(tbl.buz) end,
    buz = 1,
}

foo:bar() -- output: 1

表中的函数字段

向表中添加一个值为函数的键有两种等效的方式:

local tbl = {}

tbl.foo = function() print("hello") end

function tbl.foo()
    print("hello")
end

如果在函数定义中使用 : 替代 .,则表示函数定义的参数表中省略了第一个参数,且 第一个参数的名称为 self。下面的两种写法是等效的:

function tbl.foo(self) print(self) end

function tbl:foo() print(self) end

注意,self 并不是一个关键字,它只是一个普通的变量名。

完整的类型定义

下面展示一个完整学生类型的定义,该类型记录学生的姓名、学号,并且能够通过学号计算 学生的班级。

local Student = {}
Student.__index = Student

function Student:new(name, id)
    local obj = setmetatable({}, self)

    obj.name = name
    obj.id = id

    return obj
end

function Student:get_class_number()
    -- (2-digit year, 2-digit class, 2-digit numbering)
    return math.floor(self.id / 100) % 100
end

对上述类型定义的实例化:

local student = Student:new("陆离", 123456)
print(student:get_class_number())

模块

在 Lua 中,每个文件都是一个函数的函数体。在 Lua 脚本里,可以使用 loadfile(name) 加载 Lua 文件内容,该函数调用的返回值就是一个函数,其函数体就是文件的内容。

文件的内容被以函数的形式加载到 Lua 中之后,用户需要手动对其进行调用才能触发文件 内容的执行。

-- 使用文件的绝对或相对路径进行加载
local module = loadfile("module.lua")
module()

Lua 的模块系统是基于 loadfile 实现的。在一个脚本的最后写上 return 语句,就可以 通过调用加载文件得到函数,以函数执行返回值的形式获得脚本文件最后 return 的数值。

模块文件经常会定义一个称为 M 或者 _M 的表变量,用于在模块文件末尾返回。 其内容就是模块提供的函数、变量等。

-- foo.lua
local M = {}

function M.greet()
    print("hello")
end

return M
local foo_func = loadfile("foo.lua")
local foo = foo_func()
foo.greet()

专用于引入模块的函数 require 就是基于上述机制实现的。只不过,require 额外提供 了模块的搜索功能。调用用户提供模块名,而 require 将模块名填充到路径模式中,寻找 该模块名可能对应的文件。

路径模式是一个包含有 ? 的字符串,require 在搜索模块文件时,会将其中的所有 ? 都替换成模块名,并将替换结果作为潜在的文件名。

比如,如果 require 使用的是以下几个模式:

  • ./?.lua
  • ./?/init.lua

那么在调用 require("foo") 时,require 函数就会按顺序寻找./foo.lua./foo/init.lua 这两个文件,第一个成功找到的文件会被作为 foo 模块加载,并将 foo 文件执行的 返回值作为 require 的返回。

require 使用的模式是如何设定的在本教程中不展开讲解。

如果使用 require 函数,那么上面加载 foo.lua 模块的例子就可以改写成:

local foo = require "foo"
foo.greet()

Lua 中提供了一些内建的模块,这些模块通常不需要用户加载也可以直接使用:

  • coroutine,用于创建和管理协程
  • debug,获取 Lua 环境的调试信息
  • io,进行数据输入输出操作
  • math,提供简单的数学函数
  • os,提供非常简单的操作系统功能
  • package,控制模块加载细节
  • string,字符串的匹配、替换、格式化功能
  • table,提供对表操作的一系列功能
  • utf8,在较高版本的 Lua 中才添加的对 UTF8 字符串进行操作的模块

更进一步

如果读者对 Lua 还有继续学习的兴趣的话,请让我在此多说几句。

Lua 作为一门程序语言很小,它的设计目的是作为一门扩展语言来使用。“扩展”以两种形式 出现:

  • 使用 Lua 语言对既有的软件进行扩展,在软件中读入 Lua 脚本调用其中的功能。

    软件核心部分使用更加高性能的语言写完,并对 Lua 暴露 API,在 Lua 中写基于高性能 API 组合的、多变的业务逻辑。

    比如在游戏中,允许加载玩家编写的 Lua 脚本,作为游戏 mod 提供额外的功能。

  • 以 Lua 脚本为主要逻辑实现,使用高性能语言编写模块供 Lua 调用。

    在其它语言中使用 C 语言协作的形式,暴露核心的功能,并编译成动态库。Lua 可以直接 使用 require 函数加载动态库并调用其中的功能。

    比如深度学习库 PyTorch 的前身 Torch,就是使用 C 进行数据处理逻辑的实现,而进行 AI 实验的用户通过 Lua 实现主要的训练逻辑。

    当然,很多脚本语言都有这样的使用方式。只不过为 Lua 提供这样的库尤其简单。

Lua 的简单语法和解释器的状态机设计使得其体积小、速度快,即使将整个解释器编译到主 应用中也不会增加多少体积。用在内存大点的单片机上都不是问题,用在常规的手机、电脑 上几乎可以说是“免费”。

相对于 Lua 作为一门扩展语言的成功,作为独立语言来看,Lua 就相对贫弱了。在生态上 Lua 所拥有的库数量和种类都大幅少于其它流行的脚本语言,这一差距可以说是非常之悬殊。

第三方库的问题在 Windows 环境下更是雪上加霜,由于许多库在安装时都需要进行编译, 想要获得和在 Linux 下相同的安装体验相当有挑战性。

Lua 的官方网站是 https://lua.org/,对 Lua 有兴趣的朋友可以在这里找到 Lua 的语言 文档,以及官方推荐的相关书籍。目前该网站只提供英文。

https://atom-l.github.io/lua5.4-manual-zh/ 这是 Lua 5.4 官方文档的中文翻译,感谢 atom-l 提供了这一翻译。

Lua 的入门书籍可以参阅《Programming in Lua》,本书从最基础的概念开始,再到 Lua 中 变量、函数、数据结构的详细介绍,最后还介绍有 Lua 中面向对象编程的方式,以及让用户 能将 Lua 嵌入到自己的应用程序中的 C API,是一篇相当完整的参考书籍。本书的页数不多 读完并不需要花多少时间。

Last Update 2025-11-01