【Lua 入门系列】从顺序执行到分支与循环:一文理解 Lua 控制结构、作用域与跳转规则

🔥 本文专栏:Lua

🌸作者主页:努力努力再努力wz

💪 今日博客励志语录:真正强大的人,不是永远充满信心,而是在信心不足时,依然愿意相信下一次尝试。


思维导图:本篇文章的学习逻辑

text 复制代码
Lua 源代码
    ↓
编译为 Lua 字节码
    ↓
虚拟机解释执行字节码
    ↓
程序有一个起始执行位置,但不总是简单地线性向下执行
    ↓
控制结构:选择、重复、跳转
    │
    ├── 条件分支:if / elseif / else
    │       └── then 与 end、真假判断、嵌套 if
    │
    ├── 循环:while / repeat ... until / 数值 for
    │       ├── 先判断与后判断
    │       └── 循环控制变量、局部变量遮蔽
    │
    ├── 流程跳转:break / goto / return
    │       ├── 退出循环、跳转标签、结束函数
    │       └── goto 与局部变量的作用域限制
    │
    └── 独立代码块:do ... end
            └── 词法作用域、同名局部变量与外部变量

一、为什么我们需要控制结构?从 Lua 的执行机制说起

在前面的 Lua 学习过程中,我们已经建立了一个基本认知:CPU 不能直接将我们写的 Lua 源码当成机器指令执行。

对于标准 Lua 的执行模型,可以将这个过程理解为两个主要阶段:

  1. 编译阶段:将 Lua 源代码编译成 Lua 虚拟机能够识别的字节码,也就是一套虚拟指令。
  2. 解释执行阶段:虚拟机根据字节码中的指令执行相应操作;CPU 最终执行的仍然是解释器中早已编译好的原生机器代码。

这里我们之前已经认识过,虚拟机核心的工作可以概括为 取指、分派、执行。它遇到不同的字节码指令,就进入对应的处理逻辑。

那么,一段 Lua 脚本从哪里开始执行?

lua 复制代码
local a = 10
local b = 20

print(a)
print(b)

Lua 脚本并不要求我们像 C/C++ 那样自己定义 main()。对于这样一个脚本,程序从顶层的第一条可执行语句开始,正常情况下依次执行后面的语句。

但是,真实程序不可能永远只做这种简单的线性执行。例如,我们可能希望:

  • 只有条件满足时,才执行某一段代码;
  • 某段代码需要反复执行,而不是只执行一次;
  • 执行到特定情况时,提前结束循环或函数;
  • 直接跳转到某个指定的位置,继续执行。

这就引出了控制结构(Control Structures)。

控制结构的核心作用,就是根据程序逻辑改变正常的执行路径。 它并没有改变 Lua 源码需要经过编译与解释执行这一事实,而是让程序执行到相关结构时,可以选择某条分支、重复某段代码,或者跳过一部分指令。

从虚拟机的角度看,条件分支与循环通常会被编译成包含条件判断和跳转的字节码:条件成立时沿一条路径执行,条件不成立时沿另一条路径执行,循环则会根据需要跳回先前的位置。

这里需要区分一下:源码的书写顺序、编译处理过程,以及运行时真正执行的路径,是不同的概念。 我们下面讨论的控制结构,主要关注的是第三个:运行时的执行流程。


二、if 条件分支:重点是 Lua 的语法与 end 配对

2.1 基本语法及真假判断

先来看一个最基本的条件分支:

lua 复制代码
local score = 85

if score >= 90 then
    print("优秀")
elseif score >= 60 then
    print("及格")
else
    print("不及格")
end

这里和 C/C++ 中的 if / else if / else 没有本质的执行逻辑差别,但是需要注意 Lua 自己的语法:

  • if 后面是条件表达式,随后必须有 then;
  • Lua 使用的是一个关键字 elseif,而不是 C/C++ 的 else if;
  • else 表示前面的条件都没有成立时执行的分支;
  • 整个 if ... elseif ... else 结构,最后只需要一个 end。

程序按顺序判断条件。一旦某个分支成立,就执行该分支的代码,然后离开整个 if 结构,不再检查后面的分支。如果没有任何分支可执行,就直接继续执行 end 后面的代码。

这里还有一条很重要的规则:

Lua 只有 nil 和 false 被视为假,其余所有值都被视为真。

lua 复制代码
if 0 then
    print("0 也是真")
end

if "" then
    print("空字符串也是真")
end

if nil then
    print("这里不会执行")
end

因此,Lua 的 if 后面不要求一定是 boolean 类型的表达式。这个真假判断规则同样适用于后面将要学习的循环条件。

2.2 为什么每个 if 都必须有对应的 end?

这部分比较值得结合 C/C++ 来理解。

对于 C/C++,我们通常通过 {} 来划分代码块:

cpp 复制代码
if (a > 10) {
    printf("a > 10\n");
} else if (a > 5) {
    printf("a > 5\n");
} else {
    printf("other\n");
}

而 Lua 使用的是 then、elseif、else、end 这些关键字共同描述结构边界:

lua 复制代码
if a > 10 then
    print("a > 10")
elseif a > 5 then
    print("a > 5")
else
    print("other")
end

这里的 end 结束的是完整的 if 语法结构,而不是要求每一个分支都分别写一个 end。

为什么我们要特别关注这个问题?因为它还关联到 C/C++ 中一个经典的现象:悬空 else(Dangling Else)。

例如:

cpp 复制代码
if (a > 10)
    if (b > 10)
        printf("Hello\n");
    else
        printf("World\n");

printf("End\n");

由于这里省略了 {},else 会与最近的、尚未匹配 else 的 if 关联。因此:

  • else 属于内层的 if (b > 10);
  • 外层的 if (a > 10) 没有 else;
  • 如果外层条件不成立,整个内层结构都会被跳过,直接执行后面的 printf("End")。

Lua 中同样可以写嵌套 if,但是每一个 if 都必须有自己对应的 end:

lua 复制代码
if a > 10 then                 -- 外层 if
    if b > 10 then             -- 内层 if
        print("Hello")
    else
        print("World")        -- 这个 else 属于内层 if
    end                        -- 结束内层 if
end                            -- 结束外层 if

print("End")

如果我们想让 else 属于外层,那么必须先结束内层:

lua 复制代码
if a > 10 then
    if b > 10 then
        print("Hello")
    end                        -- 先结束内层
else                           -- 这里才是外层的 else
    print("World")
end                            -- 结束外层

如果嵌套了两个 if,却只写一个 end:

lua 复制代码
if a > 10 then
    if b > 10 then
        print("Hello")
    else
        print("World")
end

编译器会发现其中一个 if 结构没有结束,因此在编译阶段报语法错误。

这很像我们在 C/C++ 条件编译中见到的 #if ... #endif 配对形式。但请注意,两者的用途并不相同:C/C++ 的 #if 是预处理条件编译,而 Lua 的 if 是运行时条件判断。 我们只是在"开始标记与结束标记必须配对"这个形式上做类比。

本节结论 :Lua 的 if 负责条件判断,then 引出分支,elseif / else 组织后续分支,而 end 结束完整的 if 结构。嵌套多少个独立 if,就需要多少个对应的 end。


三、循环结构:从执行顺序理解 while 与 repeat ... until

前面我们通过 if 解决了"是否执行某段代码"的问题。但是如果一段代码要重复执行,就需要循环。

3.1 while:先判断条件,再执行循环体

lua 复制代码
local a = 1

while a <= 3 do
    print(a)
    a = a + 1
end

print("循环结束")

输出:

text 复制代码
1
2
3
循环结束

其语法可以概括为:

lua 复制代码
while 条件表达式 do
    -- 循环体
end

while 与 do 之间是条件表达式,do 与 end 之间是循环体。

这里的执行过程很直观:

  1. 先判断 while 后面的条件;
  2. 条件为真,执行一次循环体;
  3. 循环体执行完成后,再回到 while 重新判断;
  4. 条件为假,就退出循环,继续执行 end 后面的代码。

因此,while 可能一次循环体都不执行。它使用的真假判断规则仍然是:只有 nil 和 false 为假,其余值都为真。

另外,Lua 没有 C/C++ 中的 ++ 运算符,因此我们通常写成 a = a + 1。

3.2 repeat ... until:先执行,再判断是否退出

Lua 还提供另一种循环:

lua 复制代码
local a = 1

repeat
    print(a)
    a = a + 1
until a > 3

print("循环结束")

同样输出 1、2、3 和 循环结束。

这里有两个需要注意的地方。

第一,repeat ... until 不需要额外的 end。 repeat 标记循环开始,until 后面则写结束条件。

第二,until 条件为真时,是退出循环,而不是继续循环。

这可以直接对比 C/C++ 的 do ... while:

cpp 复制代码
do {
    // 循环体
} while (condition);  // 条件为真,继续循环
lua 复制代码
repeat
    -- 循环体
until condition       -- 条件为真,退出循环

因此,我们可以这样记忆:

  • while:满足条件,就继续循环;
  • repeat ... until:一直循环,直到满足条件才退出。

repeat 先执行循环体后判断,所以循环体至少执行一次;while 则可能一次都不执行。

3.3 一个容易忽略的区别:until 能访问循环体内声明的局部变量

这里就要联系我们之前学习过的 local 与作用域。

lua 复制代码
repeat
    local a = 10
    print(a)
until a > 5

这段代码合法,而且只会执行一次循环。原因是:repeat ... until 的局部作用域包含 until 后面的条件表达式 ,所以 until a > 5 能够访问循环体中声明的局部变量 a。

而 while 不一样:

lua 复制代码
local a = 10

while a > 5 do
    local a = 20
    print(a)
    break
end

这里 while a > 5 中的 a 是外部局部变量,循环体中的 local a = 20 是新声明的内部局部变量。

关键认知 :while 后面的条件表达式,不在其循环体内部新声明的局部变量的作用域之中;而 repeat ... until 的 until 条件能够访问循环体内已经声明、且在该位置可见的局部变量。

这会引出一个更值得单独讨论的问题:相同名字的局部变量,究竟访问的是哪一个?


四、循环与词法作用域:为什么同名变量会造成死循环?

4.1 内部可以定义和外部同名的局部变量

Lua 使用词法作用域(Lexical Scope)管理局部变量。简单来说,一个局部变量在源码中有自己可见的范围;程序在某个位置访问变量名时,会根据该位置的作用域关系找到对应的变量。

例如:

lua 复制代码
local a = 10

while a > 5 do
    local a = 20
    print(a)        -- 20
    break
end

print(a)            -- 10

这里有两个名称相同、但实际上不同的局部变量:

  • 外部 local a = 10;
  • 循环体内部 local a = 20。

进入内部变量的可见范围后,内部的 a 会**遮蔽(Shadowing)**外部的同名变量;离开内部变量的作用域后,再访问 a 就会找到外部的变量。

如果外部 a 是全局变量,同样可以被内部 local a 遮蔽:

lua 复制代码
a = 10                    -- 假设默认全局环境

while a > 5 do
    local a = 20
    print(a)              -- 20
    print(_G.a)           -- 10
    break
end

print(a)                  -- 10

需要注意:Lua 甚至允许在同一代码块内连续声明同名局部变量,后一个声明会引入新变量并遮蔽前一个,而不是像 C++ 的同一作用域重复定义那样直接报错。

lua 复制代码
local a = 10
local a = 20
print(a)                  -- 20

4.2 结合 while 分析一个典型死循环

lua 复制代码
local a = 10

while a > 5 do
    local a = 20
    a = a - 1
end

为什么这段代码会死循环?

我们按照执行过程一步一步来看:

  1. 首先创建外部局部变量 a = 10;
  2. 执行 while a > 5,此处访问的是外部 a,所以条件成立;
  3. 进入循环体,执行 local a = 20,声明新的内部 a;
  4. 执行 a = a - 1,修改的只是内部 a,由 20 变成 19;
  5. 循环体结束,再次回到 while a > 5,访问的仍然是外部 a = 10;
  6. 下一轮循环重新执行 local a = 20,又重复相同的过程。

因此,这段代码不会改变 while 条件所读取的外部变量,条件始终成立。

如果不重新声明内部 a,情况就完全不同:

lua 复制代码
local a = 10

while a > 5 do
    a = a - 1
end

print(a)                  -- 5

因为没有新的同名局部变量,这里修改的就是外部 a,循环会正常结束。

所以,真正需要建立的认知不是"循环体能不能使用外部变量",而是:当前代码位置到底能看见哪些局部变量,哪个同名变量处于更内层的可见范围。


五、数值 for:Lua 自动管理循环变量

掌握了条件循环以后,我们再来看 Lua 的数值 for。这部分和 C/C++ 最大的区别,是 Lua 把循环控制规则收敛成了起始值、终止值、步长三个表达式。

5.1 语法:初始值、终止值、步长

lua 复制代码
for i = 1, 10, 2 do
    print(i)
end

输出:

text 复制代码
1
3
5
7
9

语法可以理解为:

lua 复制代码
for 循环变量 = 初始值, 终止值, 步长 do
    -- 循环体
end

这里的循环变量名不是固定的 i ,完全可以使用 num、index、count 等合法名称:

lua 复制代码
for num = 1, 3 do
    print(num)
end

步长不写时默认是 1;步长为负数时可以倒序遍历:

lua 复制代码
for i = 5, 1, -1 do
    print(i)  -- 5 4 3 2 1(分行打印)
end

需要注意:终止值是包含式边界 ,但只有沿着给定步长恰好走到它,才会打印它。例如 1, 10, 2 不会输出 10,因为序列为 1、3、5、7、9。

对比 C/C++:

cpp 复制代码
for (int i = 1; i <= 5; ++i) {
    printf("%d\n", i);
}

Lua 数值 for 不需要自己编写更新表达式,它自动按照当前值 + 步长推进。

因此,它适合等差变化的循环。如果希望每次乘以 2,数值 for 不能直接把更新规则写成 i *= 2,可以使用 while:

lua 复制代码
local i = 1
while i <= 16 do
    print(i)
    i = i * 2
end

5.2 初始值、终止值和步长,只在循环开始时计算一次

这点和常规 C/C++ for 很不一样。

lua 复制代码
local limit = 3
local step = 1

for i = 1, limit, step do
    print(i)
    limit = 10
    step = 2
end

实际输出仍然是:

text 复制代码
1
2
3

为什么?因为 Lua 在进入循环之前,已经对三个控制表达式各求值一次,得到了这次循环要使用的起点、终点和步长。

后面即使修改 limit 和 step 两个外部变量,也不会改变已经确定的循环控制参数。

5.3 数值 for 循环变量的作用域与变量遮蔽

这里还有一个需要注意的细节:Lua 数值 for 中的循环控制变量,是由 for 自动声明的局部变量,它具有自己的作用域。

我们之前学习过 Lua 的 local 局部变量以及变量遮蔽机制,知道内部作用域允许声明与外部作用域同名的局部变量,此时内部变量会遮蔽外部变量。

而对于数值 for 来说,也是类似的道理。

例如:

lua 复制代码
local i = 100

for i = 1, 3 do
    print(i)
end

print(i)

执行结果:

text 复制代码
1
2
3
100

这里我们需要注意,虽然外部已经声明了一个局部变量 i = 100,但是执行到 for i = 1, 3 do 时,Lua 会为这个数值循环创建一个新的局部控制变量 i。

也就是说,这里实际上存在两个不同的局部变量 i,只不过它们的名字相同。

其中:

  • 外部的 i:初始值为 100,属于外部作用域。
  • 循环控制变量 i:初始值为 1,由数值 for 自动管理,在循环体内遮蔽外部的同名变量。

因此,当程序进入循环体执行 print(i) 时,访问的是循环控制变量 i,其值依次为 1、2、3。

当循环结束之后,循环控制变量的作用域也随之结束,此时再执行 print(i),访问的就是外部原本的局部变量 i = 100。

这里需要建立的关键认知就是:变量遮蔽只决定当前代码位置访问的是哪个变量,并不意味着内部变量会继承外部变量的值。

那么,如果我们希望数值 for 使用外部变量作为初始值呢?

当然也是可以的:

lua 复制代码
local i = 100

for i = i, 103 do
    print(i)
end

print(i)

执行结果:

text 复制代码
100
101
102
103
100

这里我们可以从两个阶段来理解:

首先,计算数值 for 的控制表达式。

对于 for i = i, 103 do,等号右侧的 i 访问的是外部局部变量 i = 100,因此初始值为 100,终止值为 103。

然后,进入循环执行阶段。

Lua 使用前面计算得到的初始值创建并管理新的循环控制变量 i,因此循环体内访问的是新的局部变量,依次输出 100、101、102、103。

循环结束后,外部的局部变量 i 并没有发生改变,所以最后仍然输出 100。

因此,我们可以总结为:

Lua 数值 for 的初始值、终止值以及步长表达式,可以访问外部作用域中的变量,而 for 自动声明的循环控制变量具有独立的局部作用域,会在循环体内遮蔽外部的同名变量。

同时,循环控制变量由 Lua 自动管理,我们不能通过直接给它赋值来改变数值 for 的循环进程。

这就和我们之前学习的 Lua 局部变量遮蔽机制联系起来了。

不过我们还需要更精确一点:三个控制表达式能够访问外部变量;新循环变量的作用域是循环体,而不包括这些控制表达式本身。

例如:

lua 复制代码
local i = 10
local limit = 12

for i = i, limit do
    print(i)
end

print(i)        -- 10

这里等号右边的 i 读取的是外部 i = 10,limit 读取的是外部 12;而循环体中使用的是 for 引入的新变量 i。

另外,关于循环体里能否写 i = i + 2,这里要区分语法是否允许 与是否应该这样做:

你也可以在循环体内部再声明一个同名局部变量,但它只会产生遮蔽:

lua 复制代码
for i = 1, 3 do
    local i = 100
    print(i)     -- 每轮输出 100
end

本节结论 :Lua 数值 for 由起点、终点和步长决定循环过程;三个控制表达式只在开始时求值一次;循环变量具有自己的局部作用域,由 Lua 自动控制。泛型 for 和迭代器另文讨论。


六、改变执行路径:break、goto 与 return

前面的 if 和循环,已经能够实现大部分流程控制。但有时我们需要主动提前结束或者跳转到另一个位置,这就涉及 break、goto 和 return。

6.1 break:退出最内层循环

lua 复制代码
for i = 1, 10 do
    if i == 5 then
        break
    end

    print(i)
end

print("End")

这里输出 1、2、3、4,随后输出 End。

break 的作用是:直接结束当前所在的最内层循环 ,继续执行该循环之后的语句。即使 break 写在 if 内部,它结束的也是循环,而不是仅仅结束 if。

lua 复制代码
for i = 1, 3 do
    for j = 1, 3 do
        if j == 2 then
            break
        end
        print(i, j)
    end
end

内层循环遇到 j == 2 就退出,但外层循环仍然继续。最终输出的是 (1,1)、(2,1)、(3,1)。

Lua 没有内置 continue 关键字。通常可以重新组织条件分支来跳过一轮中的某些代码;下面学习 goto 时,我们也会看到一种实现类似效果的方式。

6.2 goto:跳转到标签所在位置

goto 的语法也比较直观:

lua 复制代码
goto label

::label::

其中,label 是自定义标签名,标签的语法为前后各两个冒号:::名称::。

例如:

lua 复制代码
local a = 10

goto skip

print("这句不会执行")

::skip::
print(a)        -- 10

从运行机制来说,goto 会改变后续代码的执行位置。

不过,Lua 的 goto 不是想跳到哪里就能跳到哪里。这里有一条非常重要的限制:

不能从某个局部变量的作用域之外,通过 goto 直接跳转进入该局部变量的作用域。

来看一个会编译失败的例子:

lua 复制代码
goto skip

local a = 10

::skip::
print(a)

为什么这里会报错?我们可以结合之前的心智模型来理解:

  • 从词法作用域来看 :::skip:: 后面的位置已经位于局部变量 a 的可见范围内;
  • 从执行流程来看 :如果允许这样跳转,就绕过了 local a = 10 的声明与初始化语句;
  • 产生的问题 :程序将进入一个在词法上应当存在局部变量 a、但对应声明过程却被跳过的位置;
  • Lua 的处理方式:编译器提前禁止这样的跳转。

这里最关键的是:报错并不是由 print(a) 引起的。 即使后面不读取 a,只要 goto 进入了这个局部变量的作用域,就违反规则。

但是,并不是所有"跳过局部变量声明"的情况都不允许:

lua 复制代码
goto skip

do
    local a = 10
    print(a)
end

::skip::
print("Hello")

这段代码合法。因为 a 的作用域仅在 do ... end 内部,标签 ::skip:: 位于其作用域之外。跳转不会进入 a 的作用域。

因此,应该记住的是:限制不在于能否跳过某一行 local 声明,而在于跳转目标位置是否进入了局部变量的作用域。

另外,goto 不能跨函数边界跳转。它是从 Lua 5.2 开始引入的,Lua 5.1 不支持 goto。

利用 goto,也可以在循环中模拟跳过当前一轮剩余语句的效果:

lua 复制代码
for i = 1, 5 do
    if i == 3 then
        goto continue
    end

    print(i)

    ::continue::
end

输出 1、2、4、5。这里相当于当 i == 3 时跳转到循环体末尾,然后继续下一轮循环。

6.3 return:结束当前函数,支持多返回值

return 我们在学习函数时已经接触过了,它主要负责结束当前函数的执行,并可以返回结果。

lua 复制代码
function add(a, b)
    local result = a + b
    return result
end

print(add(10, 20))  -- 30

对于 Lua,有两个需要单独记住的特点。

第一,Lua 函数支持直接返回多个值:

lua 复制代码
function test()
    return 10, 20, 30
end

local a, b, c = test()
print(a, b, c)     -- 10 20 30

第二,return 在语法上必须位于它所在代码块的最后一条语句。

例如,下面这段代码不合法:

lua 复制代码
function test()
    return 10
    print("Hello")  -- 语法错误:return 后仍有同一代码块中的普通语句
end

但是下面这段合法:

lua 复制代码
function test(a)
    if a > 10 then
        return 100
    end

    print("继续执行")
    return 200
end

因为第一个 return 是 if 分支代码块的末尾语句,第二个 return 是函数体代码块的末尾语句。

脚本顶层也可以使用 return 来结束当前 Lua 脚本编译单元的执行;不需要为此自己定义 main()。

到这里,可以先把三种跳转语句作一个区分:

控制语句 改变了什么 需要注意的限制
break 结束最内层循环 只能用于循环
goto 跳转到指定标签 不能跳入局部变量的作用域,不能跨函数跳转
return 结束当前函数或脚本编译单元 必须位于所在代码块的最后一条语句,可返回多个值

七、do ... end:不需要条件,也可以创建独立代码块

经过前面的讨论,我们已经多次看到 do 和 end。例如 while ... do ... end、for ... do ... end。

但是 Lua 还有一种单独的写法:

lua 复制代码
do
    -- 代码块
end

它不是循环,也不是条件判断,而是显式创建一个独立的代码块。

lua 复制代码
print("开始")

do
    print("执行代码块")
end

print("结束")

上面的代码会正常依次打印三个结果。单独的 do ... end 不会自动产生循环,也不需要判断任何条件。

7.1 为什么要单独创建代码块?

最直接的用途就是:限制局部变量的可见范围。

这和 C/C++ 使用单独的 {} 代码块很相似。

cpp 复制代码
int a = 10;
{
    int a = 20;
    printf("%d\n", a);  // 20
}
printf("%d\n", a);      // 10

Lua 中可以写成:

lua 复制代码
local a = 10

do
    local a = 20
    print(a)        -- 20
end

print(a)            -- 10

内部 local a = 20 遮蔽外部的 a,但作用域只覆盖这个代码块中从声明开始的可见范围。代码块结束以后,再次访问 a 就找到外部变量。

7.2 如果内部没有重新声明 local 呢?

lua 复制代码
local a = 10

do
    a = 20
end

print(a)            -- 20

这里没有声明新的局部变量,内部的 a = 20 修改的就是外部变量。

因此,do ... end 不是把内部代码与外部代码完全隔离;它只是创建一个新的词法作用域。内部仍然可以访问和修改在该位置可见的外部变量。

7.3 do ... end 还可以配合 return 使用

前面我们说过,Lua 规定 return 必须位于当前代码块的最后一条语句。

那么,在需要提前返回的地方,可以显式引入一个代码块:

lua 复制代码
function test()
    do
        return 10
    end

    print("不会执行")
end

这是合法的,因为 return 位于内部 do ... end 代码块的最后。

需要特别注意:return 结束的是整个 test() 函数,而不只是退出 do ... end 代码块。 所以后面的 print() 不会执行。

本节结论 :do ... end 的作用是创建独立代码块,最核心的价值是管理局部变量的词法作用域。它本身不引入判断或者循环。


八、总结:把 Lua 控制结构放回统一的执行模型

现在回头来看,我们会发现这次学习的内容并不是一堆孤立的语法。

我们一开始认识 Lua 的编译与解释执行过程 ,知道虚拟机正常执行字节码,但可以通过条件与跳转改变后续执行位置;接着学习 if、while、repeat ... until 和数值 for,理解如何选择分支、如何重复执行 ;最后通过 break、goto、return 和 do ... end,把执行流程与词法作用域联系了起来。

可以将本篇的核心规则整理成下面这张表:

知识点 核心执行规则 最值得注意的 Lua 特性
if ... then ... end 根据条件选择分支 elseif 连写;每个 if 必须有自己的 end
真值判断 条件表达式决定是否执行 只有 false 和 nil 为假,0 和 "" 为真
while ... do ... end 先判断,为真才进入循环体 条件不在循环体内新声明局部变量的作用域中
repeat ... until 先执行,为真时退出 不需要额外 end;until 可以访问循环体中可见的局部变量
数值 for 按起点、终点、步长循环 三个控制表达式开始时只求值一次;循环变量独立且不应手动修改
break 退出最内层循环 不会把所有嵌套循环一起退出
goto 跳转到指定标签 不能跳入局部变量作用域;Lua 5.1 不支持
return 结束函数并返回结果 支持多返回值;必须位于所在代码块最后
do ... end 顺序执行一段独立代码块 主要用于控制局部变量的可见范围

最终我们要建立的心智模型是:程序的执行流程由控制结构决定,而在某个执行位置能够访问哪个变量,则由词法作用域决定。

这两个概念需要结合起来看,但又不能混为一谈:

  • 条件分支、循环和跳转,解决的是下一步执行哪一段代码;
  • local、代码块和变量遮蔽,解决的是在当前源码位置,变量名对应哪一个变量;
  • goto 的作用域限制,则是这两套规则相互约束的典型例子。

掌握这一层关系以后,我们再阅读 Lua 项目里的控制结构,就不只是记住 then、do、end 的用法,而是能够看懂程序如何执行,以及为什么某个变量会在此处被访问或修改。

至于 Lua 的泛型 for ,我们先放在这里。它涉及迭代器,等后续把迭代器的执行机制梳理清楚,再回来讨论 pairs()、ipairs() 与泛型 for 的关系,会更符合循序渐进的学习顺序。


相关推荐
huoyueyi12 小时前
《融天AI-APP从0到1开发实录》
开发语言·lua
xiaoye-duck21 小时前
《C++ 工具库实战》GTest 单元测试:断言、全局环境、局部夹具与底层原理
c++·单元测试·第三方库
努力努力再努力wz1 天前
【Lua入门系列】从动态类型到变量作用域:彻底理解 local、全局变量与跨文件访问
开发语言·junit·lua
小凯在掘金2 天前
测试规范:为什么优先用 data-testid,而不是 CSS 选择器?
单元测试·测试
买房动力十足2 天前
秒杀商城 + AI 运营:Lua 防超卖、MQ 削峰,AI 负责选品和策划
开发语言·人工智能·lua
2501_915106322 天前
iOS 单元测试怎么跑?用 KXApp 在 VS Code 里跑 XCTest 和 Swift Testing
ide·vscode·ios·单元测试·个人开发·swift·敏捷流程
aloha_7893 天前
Java 基础、集合、异常与 JUnit 5 速查
java·junit·sqlserver
小莫4615 天前
把「测试有效性」加进 CI:GitHub Actions 配置指南
单元测试