1.2 第一个Lua程序与语法骨架


文档摘要

1.2 第一个 Lua 程序与语法骨架 本节摘要:本节动手跑通 Lua 的两种运行形态——交互式解释器与脚本文件,然后一次过完语法骨架级的约定:大小写敏感、分号可选、两种注释、end 界定的块、保留字清单、多变量赋值。末尾给出全册第一段"被宿主调用"的脚本,把语法骨架放回寄宿现场。 动手:三分钟跑起来 先把解释器弄到手。Lua 的发行极其朴素:官网提供 C 源码,一条标准的编译命令就能出可执行文件;多数平台也有现成渠道——Windows 下可用包管理器一键安装或下载他人编译好的解释器压缩包,macOS 与 Linux 用系统包管理器装 lua 即可。装好后在终端里敲: 能看到类似下面的回应就说明到位了(版本号以你装到的为准): 直接输入 回车,进入交互式解释器,提示符是 。

1.2 第一个 Lua 程序与语法骨架

本节摘要:本节动手跑通 Lua 的两种运行形态——交互式解释器与脚本文件,然后一次过完语法骨架级的约定:大小写敏感、分号可选、两种注释、end 界定的块、保留字清单、多变量赋值。末尾给出全册第一段"被宿主调用"的脚本,把语法骨架放回寄宿现场。

动手:三分钟跑起来

先把解释器弄到手。Lua 的发行极其朴素:官网提供 C 源码,一条标准的编译命令就能出可执行文件;多数平台也有现成渠道——Windows 下可用包管理器一键安装或下载他人编译好的解释器压缩包,macOS 与 Linux 用系统包管理器装 lua 即可。装好后在终端里敲:

lua -v

能看到类似下面的回应就说明到位了(版本号以你装到的为准):

Lua 5.4.6 Copyright (C) 1994-2023 Lua.org, PUC-Rio

直接输入 lua 回车,进入交互式解释器,提示符是 >。从这里开始,每敲一行它算一行:

> print("hello, host") hello, host > 1 + 2 3 > "lua" .. 5.1 lua5.1

注意第二个例子:表达式敲进解释器,它会把值直接回显。两个点 .. 是字符串连接运算符,数字会被自动转成字符串再接上。这种"敲一行见一行"的体验是学习 Lua 的最佳途径,全册所有短示例都默认你在这种环境里验证。

交互模式有个便利设计:表达式没写完就回车,提示符会变成 >> 等你续行:

> print( >> "跨行也没关系" >> ) 跨行也没关系

退出交互模式用 os.exit(),或直接按平台对应的结束快捷键。

第二种形态是脚本文件。把代码存进一个文本文件,在命令行把文件名交给解释器:

-- 脚本内容:三行 local name = "gateway" print("booting " .. name) print(("=%s="):format(name))

命令行执行 lua 加脚本文件名,输出:

booting gateway =gateway=

脚本形态是宿主最常用的调用方式:游戏引擎启动时加载一堆脚本文件,Redis 的 SCRIPT LOAD 收一段脚本文本缓存起来,OpenResty 在配置里声明脚本路径——本质都是"把一段文本交给 Lua 虚拟机执行"。还有一种内嵌字符串形态:宿主把脚本放在 C 代码的字符串常量里直接 dostring 执行,适合极短的初始化逻辑。三种形态你在后续章节都会见到。

脚本的"块"与宿主的调用单位

Lua 把每一段被加载执行的代码叫一个(chunk):一个脚本文件是一个块,交互解释器里的每一行也是一个块,dostring 收到的字符串还是一个块。块可以很大——整个项目的启动脚本;也可以小到一行。块内的 local 变量作用到块尾,块与块之间通过全局表或宿主注入的接口通信。

理解"块"对嵌入式开发格外重要,因为宿主调用脚本的最小单位就是块。游戏引擎把每个 UI 文件当作一个块加载;Redis 把每段 EVAL 脚本作为一个块缓存;OpenResty 按 content_by_lua 等指令引用的文件为块加载。你在哪个块里声明 local,决定了这个名字能活多久、被谁看见。后续讲模块与热更新时,"块边界"会反复出场。

脚本还能接收命令行参数。用脚本形态运行时,解释器把参数收进全局表 argarg[0] 是脚本名,正负下标分别是从前数和从后数的位置参数:

-- 脚本开头打印自己收到的参数 for i = 0, #arg do print(i, arg[i]) end

命令行传入三个参数运行,输出形如:

0 script 1 -critical 2 workers=8 3 10.0.0.8

宿主世界里同样的机制换个名字出现:Redis 的 EVAL script numkeys key1 key2 arg1 arg2 把键与参数分别交给脚本的 KEYS 与 ARGV 表;游戏引擎把事件参数打包成表传给回调。"参数从外面进来"是脚本的日常,早点习惯它。

语法骨架:五分钟过完全部约定

Lua 的语法骨架小得可以在一节里讲完。逐条来,每条都顺带看它的寄宿动机。

大小写敏感printPrintPRINT 是三个不同的名字。原因不浪漫:实现上名字进哈希表,区分大小写让词法分析器少一层规则;对宿主的好处是注册进来的 C 函数名不会因为大小写折叠而撞车。

分号可选。Lua 允许语句以分号结尾,但完全不写也行:

> local a = 1 > local b = 2; local c = a + b > print(a, b, c) 1 2 3

写不写分号纯属个人与团队习惯。有个易踩的点:以括号开头的语句会被当成对上一行的函数调用,所以换行写语句时别用 ( 打头。

两种注释。单行用双横线,多行用双横线加双方括号:

-- 单行注释:宿主常用它写参数说明 --[[ 多行注释 可以跨很多行 ]]

有个高级玩法:给多行注释加等量等号(如 --[==[ ]==])可以在注释内再嵌注释内容,等级配对即可, OpenResty 配置生成器就靠这个特性拼装大段脚本文本。

--[==[ 内含 ]] 也不会提前结束 ]==] print("still alive")

字面量速查。骨架级还有一类约定值得提前备案:怎么写常量。数字支持十进制、十六进制(0xff)、二进制(0b1010,3.2 之后)与科学计数(1e35e-2);字符串支持双引号、单引号(两者完全等价,随手选用)与长括号形式:

> local doc = [[ 一行说明可以随便换行 转义序列 \n 在这里原样保留 ]] > print(#doc) 46

长括号 [[...]] 里的反斜杠不转义、换行原样保留,写多行文本、SQL、HTML 模板、注释文档时是救星。它同样支持等号分级([==[...]==]),与注释的等号分级是同一套词法规则。数字字面量在 5.3 之后还会自动带子类型(0x10 是整数,1e2 是浮点),1.3 节展开。

块用 end 界定。Lua 没有大括号,函数体、if 体、for 体都以关键字开头、以 end 收尾:

local function greet(who) if who then return "hi " .. who end end print(greet("lua"))

输出 hi luado...end 还能单独开一个作用域块,后面讲 local 时会用到。对宿主而言,end 界定的块在词法上一目了然,做代码着色、片段替换(热更新时按函数粒度换代码)都比大括号配对更稳。

保留字一共二十来个andbreakdoelseelseifendfalseforfunctiongoto(5.2 起)、ifinlocalnilnotorrepeatreturnthentrueuntilwhile。注意 repeat...until 是一对,until 后面直接跟条件。这份清单短到能背下来,也再次印证"一页文法"的说法。

标识符:字母或下划线开头,后跟字母、数字、下划线。习惯上,下划线开头的名字(如 _VERSION_G)留给语言与宿主注入的保留物,自己的变量别抢。

多变量赋值一步到位:

> local x, y = 1, 2 > x, y = y, x -- 交换,不需要中间变量 > print(x, y) 2 1 > local a, b, c = 10 -- 多余的名字得 nil > print(a, b, c) 10 nil nil

右边多了的值被丢弃,左边多了的名字得 nil。这个"多退少补"规则同样适用于函数返回值的接收,是下一章多返回值机制的语法基础。

print 是万能探针。它接受任意多个参数、用制表符分隔输出、自动调用 tostring 规则:

> print(nil, true, 3.14, "s", print) nil true 3.14 s function: 0x55e0b4a3e0b0

table 类型的值 print 出来是地址,要看内容用后面章节的 table.concat 或专门函数。排错时 print...(可变参数)能拼出简易日志器,嵌入式现场没调试器时这是最趁手的工具。

读懂第一份报错

写脚本就会遇到报错,早一点学会读报错,学习曲线会平缓很多。Lua 的错误消息格式统一:位置信息 + 企图做的事。故意制造一个:

> local n = nil > print(n.weight) input:1: attempt to index a nil value (local 'n')

拆开读:input:1 是"哪块哪行"(交互环境里块名叫 input,脚本形态则是文件名加行号);attempt to index a nil value 是"对一个 nil 值做索引";括号里的 (local 'n') 直接点名肇事变量。这份点名信息在 5.4 里相当完整——索引 nil、调用 nil、比较类型不匹配,都会告诉你具体是哪个名字。三个最高频的错误家族先混个脸熟:

> local t = {} > t.f() -- attempt to call a nil value (field 'f') > local s = "abc" > s = s + 1 -- attempt to perform arithmetic on a string value > return #nosuch -- attempt to get length of a nil value (global 'nosuch')

报错不是谴责,是路标。嵌入式现场里脚本抛错的默认结局是被宿主的错误处理接住(打印日志、报警、跳过本次调用),不会带崩宿主进程——这个"错误边界"的契约第 6 章细讲,现在你只需要知道:大胆在解释器里试错,报错的代价是零

新手第一天的高频翻车点

把社区问答里出现率最高的五个入门错误列在这里,每一条都可以立刻在解释器里复现:

其一,把 = 当比较用。Lua 的等于判定是 ==,单个 = 只出现在赋值位置,写在条件里会直接语法报错。其二,elseif 拼成 else if,程序仍能跑但要求多一个 end,块结构对不上时报错行号会指向很远处,排查费劲。其三,交互解释器里 local 跨行失效(1.3 节详细解释),新手常误以为 local 声明"不生效"。其四,注释符号写成 //#——Lua 用 --,井号在 Lua 里是长度运算符,写在行首会引发离奇的解析错误。其五,用 != 表示不等于,正确写法是 ~=。这五条都与 C 家族的肌肉记忆有关,前两周有意识提醒自己,之后自然就切换过来了。

宿主调用的第一现场

语法骨架看完了,回到寄宿视角。下面这段脚本是"宿主式"写法的雏形——它假设宿主已经注入了一个叫 host 的表,脚本通过它回调宿主能力:

-- 假设宿主注册了全局表 host,提供 log 与 config 两个接口 local profile = host.config("profile") or "default" if profile == "debug" then host.log("verbose", "running in debug profile") end local weights = { sword = 12, shield = 7, bow = 9 } local best for name, w in pairs(weights) do if not best or w > weights[best] then best = name end end host.log("info", ("best weapon %s weight %d"):format(best, weights[best]))

脚本没有调用任何"危险"库(io、os 一概未沾),只用了宿主注入的 host 表——这就是沙箱式脚本的雏形:能力全部由房东发放,房客自带行李只有语言本身。第 6 章讲标准库裁剪、第 9 章讲宿主注册 API 时,都会回到这个模型。

脚本住在宿主的哪一层

脚本住在宿主的哪一层

这张分层图值得现在就记住,全册反复出现:宿主在顶层拥有进程;C API 是唯一通道;每个 Lua 状态(lua_State)是一个独立的虚拟机实例,宿主可以同时开多个互相隔离的实例;脚本住在虚拟机里,读写到的"全局能力"其实都是宿主注册进去的函数表。同进程意味着快——没有跨进程通信;有边界意味着稳——脚本错误触发的是 Lua 错误处理,而不是进程崩溃,前提是宿主正确使用 pcall(第 6 章)。

语法骨架的寄宿注脚

把本节骨架内容压回寄宿视角收个尾。分号可选、块用 end 界定、保留字一页可数——这些"省略"共同服务于一个目标:让脚本代码成为可以被机器可靠处理的文本。宿主要对脚本做的事比人多:按块加载、按函数替换、按行数计费、做静态扫描(全局变量检查、危险调用检查)。语法的确定性越强,这些自动化就越可靠。同理,注释是宿主与脚本作者的协作通道——配置脚本头部的参数说明、插件文件头的版本与依赖声明,都是写给宿主的解析器看的。你写下的每个语法元素,都同时有两个读者:现在的人,与将来要处理这段文本的宿主机器。

本节要点回顾

  • 两种运行形态:交互解释器敲一行算一行,脚本文件是宿主的常规调用单位,另有内嵌字符串形态;
  • 骨架五件事:大小写敏感、分号可选、双横线与双括号注释、end 界定块、二十来个保留字;
  • 多变量赋值遵循"多退少补",是交换写法与多返回值接收的共同基础;
  • print 是基础探针,配合可变参数可拼简易日志,嵌入式现场没有调试器时的第一工具;
  • 宿主式脚本只依赖宿主注入的表,自带行李里不含 io/os 等危险库,这个模型贯穿第 6、9 章;
  • 分层图:宿主进程 → C API 虚拟栈 → 虚拟机状态 → 脚本,同进程有边界,快与稳兼得。

下一节清点 Lua 的全部家当:八种值类型,以及变量与作用域的规矩。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U