Skip to content

Lua 首批课程:table、闭包、模块与日志统计 ​

Lua 5.5.1 的 Windows/Linux 解释器构建与日志 CLI 测试属于原文 2026-10-17 历史记录;本轮未构建或执行。下文版本、命令及输出供读者在自己的解释器环境核对。

Lua常嵌入其他程序,但宿主接口不是Lua语言本身。本批先在独立解释器里学习:local与值→table与序列→函数/闭包→require模块→pcall和文件→完整脚本。无需先学C;只有实际嵌入C/C++宿主时才继续学C API和相应前置。

版本、安装与运行 ​

核对2026-10-17,官方当前Lua5.5.1。使用对应官方源码构建的Windows amd64和Linux amd64解释器;LuaJIT及游戏引擎中Lua5.1/5.3等实现不能自动当成同一版本。官方参考手册同时定义语言、标准库和C API;宿主可能删除io/os、注入全局或改变模块路径,教程步骤先以独立进程为准。macOS不纳入本轮验收。

安装可信来源且支持本平台的Lua5.5,lua -v应显示5.5.1;本批从官方源码下载区取源码并按公开SHA256校验,用Zig0.15.2 C工具链分别构建。普通学习者可按系统可信包管理器或官方指向的发行方式安装,不能把系统包中的较旧版本输出当成5.5验证。源码构建的完整配置与平台选项见源包README,不需全局安装第三方Lua模块。

保存hello.lua,执行lua hello.lua:

lua
local count = 0
for _, score in ipairs({59, 60, 100}) do
    if score >= 60 then count = count + 1 end
end
assert(count == 2)
print("passed=" .. count)

预期passed=2。..连接字符串,==比较相等,~=是不等;Lua的索引通常从1开始,但table允许其他键。文件是脚本,命令行不是放进脚本的语言语法。

local、动态值与真假 ​

变量没有固定类型,值有类型;nil、boolean、number、string、table、function等不同。未加local的赋值可能改全局环境,模块与函数内优先显式local。只有nil和false为假,0和空字符串都为真;不能照搬其他语言的空值判断。

保存values.lua,执行成功无输出:

lua
local enabled = false
local missing = nil
assert(enabled ~= nil and missing == nil)
assert(0)
assert("")
local config = {enabled = false}
assert(config.enabled == false)
local fallback = config.enabled or true
assert(fallback == true)

最后的or会把合法false也替换掉;需要只为nil提供默认值时,显式if value == nil then ... end。number包含整数与浮点数,math.type可区分;整数范围依构建,不把大整数当无上限。字符串按字节保存,#text是字节长度,UTF-8视觉字符数量需另行定义。

table:映射、序列与引用 ​

table是主要数据结构;赋值共享同一个table,不自动复制。序列是从1连续到n的非nil项,ipairs按这一前缀迭代;pairs遍历键值但不保证顺序。带空洞的table不能用#推断所有有效元素数量。

lua
local original = {10, 20, 30}
local alias = original
alias[1] = 99
assert(original[1] == 99)
local sparse = {[1] = 10, [3] = 30}
local visited = 0
for _ in ipairs(sparse) do visited = visited + 1 end
assert(visited == 1)
local settings = {name = "demo", enabled = false}
assert(settings["name"] == settings.name)
settings.name = nil
assert(settings.name == nil)

保存table-demo.lua运行,预期无输出。将值设nil删除键;false仍是存在的值。本项目检查连续序列,不悄悄忽略索引3的记录。浅复制只复制顶层键值,嵌套table仍共享;是否深复制由业务决定。

函数、闭包与模块 ​

函数也是值,闭包可保留外围局部绑定。保存closure.lua运行,预期无输出:

lua
local function make_counter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end
local first, second = make_counter(), make_counter()
assert(first() == 1 and first() == 2 and second() == 1)

.调用普通函数,:定义/调用方法时隐式传self,不能随意混用。函数可返回多个值,但表达式位置会影响取值数量,检查错误接口时先读文档;例如io.open返回文件或nil和原因。

模块通常返回一张接口表;local summary = require("summary")按package.path/package.cpath查找并缓存模块结果,不是任意文件路径执行器。进入本项目目录再运行CLI,使默认./?.lua能找到summary.lua。文件名别叫io.lua或table.lua。不要为读取用户配置调用load/dofile执行其中代码,数据与可执行脚本需分开。

完整日志计数模块与CLI ​

路径examples/lua/log-summary。每行是INFO、WARN或ERROR,后跟空格/Tab和非空消息;消息最多160字节、行最多256字节、10000行。接受CRLF,空文件输出三个零;空行、其他级别与超限失败。计数不解析消息编码,不伪称完整日志格式或JSON解析。只读文件。

summary.lua(完整模块)
lua
local M = {}

local function initial()
    return {INFO = 0, WARN = 0, ERROR = 0}
end

local function add(counts, line, row)
    if type(line) ~= "string" or #line > 256 or row > 10000 then error("input limit exceeded", 0) end
    line = line:gsub("\r$", "")
    local level, message = line:match("^(%u+)[ \t]+(.+)$")
    if not level or counts[level] == nil or #message > 160 or not message:match("%S") then
        error("row " .. row .. ": invalid log record", 0)
    end
    counts[level] = counts[level] + 1
end

function M.count(lines)
    local count, maximum = 0, 0
    for index in pairs(lines) do
        if type(index) ~= "number" or math.type(index) ~= "integer" or index < 1 or index > 10000 then
            error("expected dense sequence", 0)
        end
        count = count + 1
        maximum = math.max(maximum, index)
    end
    if maximum ~= count then error("expected dense sequence", 0) end
    local counts = initial()
    for index = 1, count do add(counts, lines[index], index) end
    return counts
end

function M.scan(file)
    local counts, row = initial(), 0
    while true do
        local line, read_error = file:read("l")
        if line == nil then
            if read_error then error(read_error, 0) end
            break
        end
        row = row + 1
        add(counts, line, row)
    end
    return counts
end

return M
cli.lua
lua
local summary = require("summary")
if #arg ~= 1 then
    io.stderr:write("usage: lua cli.lua FILE\n")
    os.exit(2)
end
local file, open_error = io.open(arg[1], "rb")
if not file then
    io.stderr:write("error: " .. open_error .. "\n")
    os.exit(1)
end
local ok, result = pcall(summary.scan, file)
local closed, close_error = file:close()
if not ok or not closed then
    io.stderr:write("error: " .. tostring(ok and close_error or result) .. "\n")
    os.exit(1)
end
print(("INFO=%d WARN=%d ERROR=%d"):format(result.INFO, result.WARN, result.ERROR))
test_summary.lua
lua
local summary = require("summary")
local counts = summary.count({"INFO started", "WARN retry", "INFO done"})
assert(counts.INFO == 2 and counts.WARN == 1 and counts.ERROR == 0)
assert(summary.count({}).INFO == 0)
for _, lines in ipairs({{"DEBUG msg"}, {"INFO "}, {""}, {"INFO " .. string.rep("x", 161)}}) do
    local ok = pcall(summary.count, lines)
    assert(not ok)
end
local sparse = {[1] = "INFO first", [3] = "INFO third"}
assert(not pcall(summary.count, sparse))
print("summary tests passed")
sample.log.txt
text
INFO started
WARN retry
INFO done

在目录内执行lua cli.lua sample.log.txt,预期INFO=2 WARN=1 ERROR=0。执行lua test_summary.lua,预期summary tests passed。测试包括正常/空输入、错误级别/消息/长度和稀疏序列;真实CLI另验证缺文件、参数缺失、坏行和空文件。输出显式按INFO/WARN/ERROR排列,不依赖pairs顺序。

pcall、关闭文件与排错 ​

pcall返回成功标记及结果,失败时返回false和错误对象;它不自动关闭函数里打开的所有资源。CLI在打开文件后用pcall调用scan,无论解析成功与否都关闭文件;关闭失败也返回错误。库函数借用文件句柄,不接管它的生存期。输入逐行读取后检查长度,不承诺超大单行的固定内存占用。

成功退出0写stdout;缺少/多余参数2向stderr写usage;文件/格式/读取或文件关闭错误1向stderr写error,不输出空计数冒充成功。module 'summary' not found先检查工作目录、大小写和package.path;attempt to index a nil value先追查缺值来源;and/or不是在所有值上都等价于三元运算。pcall处理不了进程被强行终止,也不能把不可信代码变成安全沙箱。

协程是可协作暂停的执行单元,不等于自动并行线程;具体调度和IO能力由宿主/运行库决定。本批先完成模块边界,不要求协程、元表或C API才能开始项目。

练习与参考 ​

  1. 给ERROR加两条日志,预测完整输出。2. 增加DEBUG,改哪些位置才能保持输入校验与输出一致?3. 解释稀疏表和false配置为何不能用#或or默默处理。4. 从另一目录运行CLI,如何在不修改全局环境的前提下处理模块路径?
提示与参考解法

原样本加两条ERROR后为INFO=2 WARN=1 ERROR=2。新增级别要同步initial、输出格式、测试和文档,不仅放宽正则;计数检查用counts[level]~=nil。稀疏表的边界不是有效条目总数,false是有效业务值;默认值只在nil分支设置。初学路径先cd到项目目录;工程项目可在启动器明确设置项目模块搜索路径并解释影响,不把每个模块都写成依赖调用者任意当前目录的脚本。

依据:Lua5.5官方手册、官方版本历史、Programming in Lua。国内讲解对照菜鸟Lua教程的组织方式,不将未明确5.5版本的教程当版本依据;本篇规则以5.5官方手册核对。

继续阅读