cv.cm

编码 · 编程语言

Lua 入门教程

阅读约 9 分钟

简短回答

Lua 是一门轻量、可嵌入的脚本语言,用于游戏、Roblox(使用其 Luau 方言)、Neovim、Redis 和嵌入式设备。它语法精简,只有一种数据结构——表(table),可同时充当数组、字典和对象。本页涵盖 Lua 的安装、变量与类型、流程控制、函数、表、元表、用 pcall 处理错误,以及一个完整示例。

Lua 是什么,用来做什么

Lua 是一门小巧、快速的动态类型脚本语言,1993 年诞生于巴西的大学 PUC-Rio。它的整个解释器是一个用 C 编写的精简库,因此很容易嵌入到更大的程序中。它只有表这一种数据结构,概念很少,学起来很快。

Lua 主要用作嵌入式脚本语言:游戏引擎和游戏(Roblox 使用名为 Luau 的 Lua 方言;LÖVE 和 Defold 使用 Lua)、Neovim 编辑器、Redis 数据库、搭配 OpenResty 的 Nginx 等网络工具,以及许多嵌入式设备。LuaJIT 是流行的 Lua 5.1 代码即时编译器。

安装 Lua 并运行 Hello, world

用包管理器安装 Lua(例如 brew install lua 或 apt install lua5.4),或从 lua.org 下载源码编译。单独运行 lua 可进入交互式提示符。

-- hello.lua
print("Hello, world!")
lua hello.lua
# Hello, world!

注释以 -- 开头。块注释写作 --[[ ... ]]。语句不需要分号。

变量与基本类型

变量默认是全局的。除非确实需要全局变量,否则请始终用 local 声明。Lua 有八种基本类型,下面列出日常最常用的几种。

local name = "Lua"
local year = 1993              -- integer
local pi = 3.14159             -- float
local ok = true
local nothing = nil

print(type(name), type(year), type(nothing))  -- string  number  nil
print(math.type(year), math.type(pi))         -- integer  float
print(name .. " " .. year)                    -- Lua 1993 (.. joins strings)
print(#name)                                  -- 3 (length)
print(7 / 2, 7 // 2, 7 % 2, 2 ^ 10)           -- 3.5  3  1  1024.0
print(string.format("%.2f", pi))              -- 3.14
print(tonumber("42") + 1, tostring(10))       -- 43  10
类型示例说明
nilnil表示没有值;未设置的变量为 nil
booleantrue, false只有 false 和 nil 为假;0 和 "" 为真
number42, 3.14自 Lua 5.3 起分为整数和浮点数两种子类型
string"hi", 'hi', [[long]]不可变的字节串
table{1, 2, x = 3}唯一的数据结构
functionfunction() end一等公民的值

不等于运算符是 ~=,而不是 !=。逻辑运算符是单词 and、or 和 not。

流程控制

代码块以关键字 end 结束。Lua 有 if/elseif/else、while、repeat ... until、数值 for,以及配合迭代器使用的泛型 for。

local score = 72
if score >= 90 then
  print("A")
elseif score >= 70 then
  print("B")                   -- B
else
  print("C")
end

for i = 1, 3 do                -- 1 to 3, inclusive
  io.write(i, " ")             -- 1 2 3
end
print()

for i = 10, 1, -4 do io.write(i, " ") end   -- 10 6 2
print()

local n = 3
while n > 0 do n = n - 1 end

repeat
  n = n + 1
until n >= 2
print(n)                       -- 2

Lua 没有 continue 语句;可以用 goto continue 配合 ::continue:: 标签,或用 if 重新组织循环。

函数

函数也是值。它们可以用 ... 接收可变数量的参数,并返回多个值。闭包会捕获外层作用域的局部变量。

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

local function minmax(t)
  local lo, hi = math.huge, -math.huge
  for _, v in ipairs(t) do
    lo, hi = math.min(lo, v), math.max(hi, v)
  end
  return lo, hi                -- two return values
end

local function counter()
  local count = 0
  return function()            -- a closure
    count = count + 1
    return count
  end
end

print(add(2, 3))               -- 5
local lo, hi = minmax({4, 9, 1})
print(lo, hi)                  -- 1  9
local next_id = counter()
print(next_id(), next_id())    -- 1  2
local function sum(...)
  local total = 0
  for _, v in ipairs({...}) do total = total + v end
  return total
end
print(sum(1, 2, 3))            -- 6

表:数组、字典与对象

表是关联数组,可以用除 nil 之外的任何值作为索引。同一种结构可以充当数组、字典、集合、模块或对象。按惯例,数组从索引 1 开始。

local fruits = {"apple", "banana"}
table.insert(fruits, "cherry")
print(#fruits, fruits[1])            -- 3  apple

for i, f in ipairs(fruits) do        -- array part, in order
  print(i, f)
end

local ages = {ann = 31, bo = 25}
ages["cy"] = 40
ages.bo = nil                        -- remove a key
for k, v in pairs(ages) do           -- all keys, order not defined
  print(k, v)
end

table.sort(fruits, function(a, b) return a > b end)
print(table.concat(fruits, ", "))    -- cherry, banana, apple

元表与面向对象的 Lua

元表是 Lua 的标志性特性。附加到表上的元表会改变它的行为:__index 提供缺失的键(用于继承),而 __add、__eq、__tostring 等字段可以重载运算符。冒号语法 obj:method() 会把对象作为隐藏的第一个参数 self 传入。

local Vector = {}
Vector.__index = Vector

function Vector.new(x, y)
  return setmetatable({x = x, y = y}, Vector)
end

function Vector:length()
  return math.sqrt(self.x ^ 2 + self.y ^ 2)
end

Vector.__add = function(a, b) return Vector.new(a.x + b.x, a.y + b.y) end
Vector.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")" end

local v = Vector.new(3, 4) + Vector.new(0, 0)
print(tostring(v))       -- (3, 4)
print(v:length())        -- 5.0

Lua 还有协程(coroutine.create、coroutine.resume、coroutine.yield),可以让函数暂停并在稍后恢复。游戏脚本常用它们来实现跨越多帧的分步行为。

错误处理

用 error(message) 抛出错误。用 pcall(受保护调用)捕获,它不会中止程序,而是返回 false 和错误信息。许多库函数不抛出错误,而是返回 nil 和错误消息,如 io.open。

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

local ok, result = pcall(divide, 10, 2)
print(ok, result)                  -- true  5.0

ok, result = pcall(divide, 1, 0)
print(ok, result)                  -- false  script.lua:3: division by zero

local f, err = io.open("missing.txt", "r")
if not f then print("open failed: " .. err) end

错误消息中的文件名和行号取决于代码运行的位置。当 value 为 false 或 nil 时,assert(value, message) 会抛出错误,是检查结果的简便方法。

一个完整的小程序:词频统计

这个程序用模式匹配统计文本中的单词,按频率排序并输出前三名。它用到了字符串模式、表、自定义比较函数排序和字符串格式化。

local text = [[
The cat and the dog. The dog sat,
and the cat ran.
]]

local counts = {}
for word in text:lower():gmatch("%a+") do
  counts[word] = (counts[word] or 0) + 1
end

local list = {}
for word, n in pairs(counts) do
  list[#list + 1] = {word = word, n = n}
end
table.sort(list, function(a, b)
  if a.n ~= b.n then return a.n > b.n end
  return a.word < b.word
end)

for i = 1, 3 do
  print(string.format("%-4s %d", list[i].word, list[i].n))
end
-- the  4
-- and  2
-- cat  2

接下来学什么

  • lua.org 上的 Lua 参考手册,简短而完整。
  • Roberto Ierusalimschy(Lua 的作者之一)所著的《Programming in Lua》,第一版可在线免费阅读。
  • 你所用宿主环境的文档,如 Neovim 的 Lua 指南、LÖVE 或 Roblox 的 Luau 文档。
  • LuaRocks:Lua 模块的包管理器。

常见问题

为什么 Lua 的数组从 1 开始?

Lua 的设计者选择从 1 开始的索引,以符合常见的数学习惯和非程序员的习惯。ipairs、# 和 table.insert 等标准库函数都假定序列从 1 开始。

Lua 比 Python 快吗?

标准 Lua 解释器小巧,就动态语言而言通常很快,而 LuaJIT 在许多场景下要快得多。实际速度取决于具体任务,请针对自己的场景实测。

pairs 和 ipairs 有什么区别?

ipairs 从索引 1 开始按顺序遍历数组部分,直到遇到第一个 nil。pairs 以不确定的顺序访问表中的每个键。

Luau 是什么?

Luau 是由 Roblox 开发、派生自 Lua 5.1 的语言。它增加了渐进类型和性能改进,用于 Roblox 游戏。

Lua 适合初学者吗?

适合。它的语法小巧一致、概念很少,因此在游戏模组制作和教学中很受欢迎。它的标准库极简,更多功能需要依赖宿主程序或模块。

全部编码教程