Lua语言程序设计2:函数、输入输出、控制结构
文章目录
- 第六章:函数
- 6.1 基础概念
- 6.2 返回多个值
- 6.3 可变长参数
- 6.4 常用函数
- 6.4.1 table.pack
- 6.4.2 `select`
- 6.4.3 `table.unpack`
- 6.5 尾调用
- 七:输入输出
- 7.1 简单I/O模型
- 7.2 完整I/O模型
- 7.2.1 基础函数
- 7.2.2 典型应用
- 7.3 OS库
- 7.4 习题
- 第八章:控制结构
- 8.1 变量
- 8.1.1 局部变量 VS 全局变量
- 8.1.2 局部变量
- 8.2 控制结构
- 8.2.1 if 条件语句
- 8.2.2 循环语句
- 8.2.2.1 `while`循环
- 8.2.2.2 `repeat-until`循环
- 8.2.2.3 `for`循环
- 8.3 跳转语句
- 8.3.1 break和return
- 8.3.2 goto语句
- 8.3.2.1 基础用法
- 8.3.2.2 局限性
- 8.4 练习题解答
第六章:函数
6.1 基础概念
-
基本概念与语法:函数是语句和表达式抽象的主要机制,在Lua中,函数可以执行特定任务——类似其他语言的过程( procedure)或子程序(subroutine);或者是计算并返回值。无论哪种情况,函数调用时都需要使用一对圆括号把参数列表括起来。即使调用时不需要参数,也需要一对空括号
()
,唯一例外的是当函数只有一个参数且为字面字符串或表构造器时,圆括号可省略。print "Hello World" <--> print("Hello World") dofile 'a.lua' <--> dofile ('a.lua') print [[a multi-line <--> print([[a multi-line message]] message]]) f{x=10, y=20} <--> f({x=10, y=20}) type{} <--> type({})
-
面向对象调用:Lua 也为面向对象风格的调用提供了一种特殊的语法,即冒号操作符。形如
o:foo(x)
的表达式意为调用对象o
的foo
方法。 -
函数定义:一个Lua程序可以调用Lua语言或C语言等其它语言编写的函数(Lua标准库中所有的函数就是C语言写的),调用时没有任何区别。Lua 语言中的函数定义的常见语法格式形如下,整个函数块分为函数名、参数列表和函数体三部分:
-- 对序列'a'中的元素求和 function add (a)local sum = 0for i = 1, #a dosum = sum + a[i]endreturn sum end
- 缩进:Lua语言对缩进没有强制要求,但良好的缩进可以提高代码的可读性。通常使用2个或4个空格作为缩进。
- end:Lua中用于结束代码块的关键字。它用于结束函数定义、循环、条件语句等。
-
参数处理:当实参数量与形参数量不同时,Lua会进行自动调整——丢弃多余实参,以及为不足的实参提供nil:
function f (a, b) print(a, b) end f() --> nil nil f(3) --> 3 nil f(3, 4) --> 3 4 f(3, 4, 5) --> 3 4 (5 被丢弃)
利用此特性可以设置默认参数:
function incCount (n)n = n or 1 globalCounter = globalCounter + n end
调用
incCount()
时,n
初始化为nil
,or
表达式返回第二个操作数1
,实现默认参数效果
6.2 返回多个值
Lua的一个非常便利的特性是函数可以返回多个结果,多个返回值用逗号分隔写在return
语句后。
function maximum(a)local mi = 1 -- 初始索引local m = a[mi] -- 初始最大值for i = 1, #a doif a[i] > m thenmi = i; m = a[i]endendreturn m, mi -- 返回最大值及其索引
end
print(maximum({8,10,23,12,5})) --> 23 3
如果函数中没有return语句,或者return语句没有返回任何值,那么函数调用的结果为nil。例如:
function foo0()
end
返回值调整规则:Lua语言根据函数的被调用情况调整返回值的数量,下面举例说明。
function foo0 () end -- 不返回结果
function foo1 () return "a" end -- 返回1个结果
function foo2 () return "a", "b" end -- 返回2个结果
-
作为语句调用时:丢弃所有返回值
foo2() -- 两个返回值都被丢弃
-
作为多重赋值被调用时,如果是最后一个或唯一一个表达式,函数调用将产生尽可能多的返回值,按顺序赋值给变量。如果返回结果数量少于变量数量,多余的变量会被赋值为nil:
x = foo2() -- x="a", "b"被舍弃 x, y = foo2() -- x="a", y="b" x, y, z = 10, foo2() -- x=10, y="a", z="b"x, y = foo0() -- x=nil, y=nil x, y = foo1() -- x="a", y=nil x, y, z = foo2() -- x="a", y="b", z=nil
如果是多重赋值的非最后一个表达式,那么只返回第一个结果,其余结果被丢弃。例如:
x, y = foo2(), 10 -- x="a", y=10 ("b"被丢弃) x, y = foo0(), 20, 30 -- x=nil, y=20 (30被丢弃)
-
作为另一个函数的实参被调用时,如果是最后一个参数,返回所有结果作为参数传递。例如:
print(foo0()) --> (no results) print(foo1()) --> a print(foo2()) --> a b
如果不是最后一个参数,只返回第一个结果(类似多重赋值的规则)。例如:
print(foo2(), 1) -- a 1 print(foo2() .. "x") --> ax
当我们调用
f(g())
时,如果f
的参数是固定的,那么Lua语言会把g
返回值的个数调
整成与f
的参数个数一致。这并非巧合,实际上这正是多重赋值的逻辑。 -
作为表构造器的一部分,如果是最后一个表达式,返回所有结果作为表的元素。例如:
t = {foo0()} -- t = {} (空表) t = {foo1()} -- t = {"a"} t = {foo2()} -- t = {"a", "b"}
如果不是最后一个表达式,只返回第一个结果。例如:
t = {foo0(), foo2(), 4} -- t[1] = nil, t[2] = "a", t[3] = 4
-
作为return语句的表达式时,返回所有结果,例如:
function foo(i)if i == 0 then return foo0()elseif i == 1 then return foo1()elseif i == 2 then return foo2()end endprint(foo(0)) -- (no results) print(foo(1)) --> a print(foo(2)) --> a b print(foo(3)) -- (no results)
-
强制返回单个结果:如果希望函数调用只返回一个结果,可以将调用结果用额外的括号括起来。例如:
print((foo0())) --> nil print((foo1())) --> a print((foo2())) --> a
从这可以意识到,return语句中不需要添加括号,否则Lua会将其视为额外的括号,从而只返回一个结果。例如
return (foo2())
,只返回第一个结果。
6.3 可变长参数
Lua支持定义可变长参数函数(Variadic Functions),即函数可以接受任意数量的参数,例如:
function add(...)local s = 0for _, v in ipairs{...} dos = s + vendreturn s
endprint(add(3, 4, 10, 25, 12)) --> 54
在这个例子中,...
表示函数可以接受任意数量的参数,也称为额外参数(extra argument)。表达式{...}
的结果是一个由所有可变长参数组成的列表,然后通过ipairs
进行遍历。
可变长参数可以通过...
表达式(也称为可变长参数表达式,vararg expression )直接访问,它会返回所有额外的参数,例如下面的函数会直接返回所有传入的参数:
function id(...) return ... end
此外,...
也可以用于多值赋值:
local a, b = ...
可变长参数可以模拟Lua语言中普通的参数传递机制,例如function foo (a, b, c)
,可改写为:
function foo (...)local a, b, c = ...
在调试时,一种常见的技巧是为某个函数添加一个额外的打印功能,以查看该函数调用时的参数
function foo1(...)print("calling foo:", ...)return foo(...)
end
假设 foo 是一个简单的函数:
function foo(a, b)return a + b
endfoo1(3, 4) -- calling foo: 3 4
当需要了解某个函数被调用时的具体参数时,可以通过这种方式快速查看。在生产环境中,可以将打印语句替换为日志记录语句,记录函数调用的详细信息。
参数混用:可变参数函数可以同时包含固定参数和可变参数。固定参数位于...
之前,Lua会先将传入的参数赋值给固定参数,剩余的参数则作为可变参数处理。例如:
function fwrite(fmt, ...)return io.write(string.format(fmt, ...))
end
在这个例子中,fmt是固定参数,而…表示可变参数。
6.4 常用函数
6.4.1 table.pack
在Lua中,使用{...}
可以将可变参数收集到一个表中,然后进行遍历。但如果参数中包含nil
,这种方法可能会导致问题,因为无法区分表中的nil
是原始参数中的nil
还是表的结束标志,此时可使用table.pack
。此函数可以将所有参数收集到一个表中,并在表中添加一个额外的字段n
,表示参数的总数,这样即使参数中包含nil
,也能正确处理。
-- 使用table.pack检查参数中是否有nil
function nonils(...)local arg = table.pack(...)for i = 1, arg.n doif arg[i] == nil then return false endendreturn true
endprint(nonils(2,3,nil)) --> false
print(nonils(2,3)) --> true
print(nonils()) --> true
print(nonils(nil)) --> false
6.4.2 select
另一种遍历函数的可变长参数的方法是使用函数select
。此函数接受一个固定的选择器参数selector
和任意数量的额外参数
- 如果
selector
是数字n
,select
函数返回从第n
个参数开始的所有参数。 - 如果
selector
是字符串#
,select
函数返回额外参数的总数量。
print(select(1, "a", "b", "c")) --> a b c
print(select(2, "a", "b", "c")) --> b c
print(select(3, "a", "b", "c")) --> c
print(select("#", "a", "b", "c")) --> 3
print(select("#", "a", nil, "c")) --> 3
当参数较少时,由于select函数可以避免创建新表,所以可以提高性能。上述add
函数可以优化为:
function add(...)local s = 0for i = 1, select("#", ...) dos = s + select(i, ...)endreturn s
end
当参数很多时,多次调用select的开销会大于创建表的开销,因此使用{...}
方法会更优。
6.4.3 table.unpack
table.unpack
:pack
把参数列表转换成一个表,与之相反,unpack
用于解包表中的值,其主要用途为:
-
赋值给多个变量:
print(table.unpack{10,20,30}) --> 10 20 30 print(table.unpack{10,nil,30}) --> 10 nil 30 a,b = table.unpack{10,20,30} -- a=10, b=20, 30被丢弃
-
将参数动态地传递给另一个函数(泛型调用,即动态调用具有任意数量的函数)。
f(table.unpack(a))
表示通过数组a
传入可变的参数来调用函数f
。所以以下函数:print(string.find("hello", "ll"))
可构建成动态函数调用:
f = string.find a = {"hello", "ll"} print(f(table.unpack(a)))
-
打印可变长参数函数的部分参数(练习6.2和6.3):
function skipFirst(...)local args = {...}table.remove(args, 1) -- 移除第一个元素-- table.remove(args) -- 移除最后一个元素return table.unpack(args) end
虽然table.unpack
是用C语言实现的,但也可以用Lua通过递归实现:
function unpack(t, i, n)i = i or 1n = n or #tif i <= n thenreturn t[i], unpack(t, i + 1, n)end
end
- 在第一次调用该函数时,只传入一个参数,此时
i=1
,n
为序列长度。函数返回t[1]
及unpack(t,2,n)
返回的所有结果 - unpack
(t,2,n)
返回t[2]
及unpack(t,3,n)
返回的所有结果 - 依此类推,直到处理完
n
个元素为止
6.5 尾调用
当一个函数调用另一个函数作为其最后一个动作时,这个调用被称为尾调用(Tail Call)。下面的例子中,f
调用g
后就直接返回了,没有其他操作:
function f(x)x = x + 1return g(x) -- 这是一个尾调用
end
尾调用消除:在尾调用的情况下,f
(调用函数)在调用g
(被调用函数)后不需要保留f
的任何上下文信息,因为f
已经完成了所有需要做的事情。当g
返回时,程序的执行路径会直接返回到调用f
的位置。因此,尾调用不消耗额外的栈空间,我们就将这种实现称为尾调用消除(tail-call elimination )。由于尾调用不消耗额外的栈空间,因此可以进行无限嵌套的尾调用而不会导致栈溢出(无限递归)。例如,下面的函数可以接受任意大的n
作为参数,而不会导致栈溢出。
function foo(n)if n > 0 thenreturn foo(n - 1) -- 尾调用end
end
正确的尾调用:
return g(x)
以下情况不是尾调用:
g(x) -- 调用后还需要返回
return g(x) + 1 -- 调用后还需要进行加法操作
return x or g(x) -- 调用后还需要进行逻辑或操作
return (g(x)) -- 调用后还需要调整返回值的数量
七:输入输出
Lua的标准I/O库提供了两种文件操作模型:简单模型(Simple I/O Model)和完整模型(Complete I/O Model)。简单模型假设有一个当前输入流和一个当前输出流,所有的I/O操作都基于这两个流。初始时,当前输入流是标准输入(C语言中的stdin),当前输出流是标准输出(C语言中的stdout)。
7.1 简单I/O模型
-
io.write
:接受任意数量的字符串或数字,将它们写入当前输出流。
使用时,避免频繁的字符串拼接,比如io.write(a..b..c)
。应该使用多参数调用的形式,比如io.write(a, b,c)
。> io.write("sin(3) = ", math.sin(3), "\n") --> sin(3) = 0.14112000805987 > io.write(string.format("sin(3) = %.4f\n", math.sin(3))) --> sin(3) = 0.1411
与
print
不同,io.write
不会自动添加换行符、制表符等格式字符,也不会自动为其参数调用tostring
,适合需要精确控制输出的场景。 -
io.read
:从当前输入流读取数据,其行为由参数控制:"a"
:从当前位置读取整个文件。如果当前处于文件的末尾或文件为空,那么返回空字符串。"l"
:读取下一行(不包含换行符),在文件末尾时返回nil
;"L"
:读取下一行(包含换行符),在文件末尾时返回nil
;"n"
:读取一个数字,如果无法读取,返回nil
;- 数字(比如
num
):以字符串读取num
个字符,无法读取时返回nil。
-- 1. 逐行读取并编号: for count = 1, math.huge dolocal line = io.read("L")if line == nil then break endio.write(string.format("%6d ", count), line) end
逐行迭代更简单的方法是使用
io.lines
:local count = 0 for line in io.lines() docount = count + 1io.write(string.format("%6d ", count), line, "\n") end
-- 2. 将文件从stdin高效复制到stdout while true dolocal block = io.read(2^13) -- block size is 8Kif not block then break endio.write(block) end
-- 3. 测试是否到达了文件末尾。是则返回nil,否则返回空字符串 io.read(0)
read
函数可以一次指定多个选项,函数根据每个参数返回对应的结果。假设一个文件每一行都有三个数字:6.0 -3.23 15e12 4.3 234 1000001 ...
-- 4. 打印每一行的最大值 while true dolocal n1, n2, n3 = io.read("n", "n", "n")if not n1 then break endprint(math.max(n1, n2, n3)) end
-
io.input,io.output
:用于设置当前输入流和输出流。io.input("input.txt") -- 设置当前输入流为文件input.txt(只读模式) io.output("output.txt") -- 设置当前输出流为文件output.txt
-- 假设 input.txt 内容为:" 3.14 abc" io.input("input.txt") io.read("n") -- 返回 3.14 (number 类型) io.read("a") -- 返回 " abc" (剩余内容)
在Lua语言中编写过滤器(filter)的一种简单技巧就是将整个文件读取到一个字符串中,然后对字符串进行处理:
t = io.read("a") -- 读取整个文件 t = string.gsub(t, "bad", "good") -- 替换内容 io.write(t) -- 写入输出流
另一个简单示例是排序文件内容:
local lines = {} -- 将所有行读取到表lines中 for line in io.lines() dolines[#lines + 1] = line endtable.sort(lines)-- 输出所有行 for _, l in ipairs(lines) doio.write(l, "\n") end
7.2 完整I/O模型
Simple I/O Model
适用于简单的单文件读写(如日志处理),如果需要同时操作多个文件、二或者更精细控制时(如二进制模式、文件指针定位、缓冲设置等),就需要用到Complete I/O Model
。
功能 | Simple Model | Complete Model |
---|---|---|
多文件操作 | ❌ 只能操作当前流 | ✅ 支持多个文件 |
二进制模式 | ❌ 默认文本模式 | ✅ 支持 "b" 模式 |
文件指针控制 | ❌ 无 | ✅ seek() |
缓冲控制 | ❌ 无 | ✅ setvbuf() |
错误处理 | ❌ 直接报错 | ✅ 返回 nil + err |
适用场景 | 简单脚本 | 复杂文件操作 |
完整I/O模型基于文件句柄(File Handle),每个文件操作都通过显式的文件对象(流)进行,而不是依赖全局的 io.input()
和 io.output()
。 底层使用 C 标准库的文件操作函数fopen
/fread
/fwrite
,可以同时读写多个文件,而不会互相干扰。下面介绍主要函数与方法。
7.2.1 基础函数
-
io.open(filename, mode)
:用于打开文件并返回文件句柄(FILE*
的 Lua 封装)。成功返回文件对象,失败返回nil + 错误信息 + 错误码
。mode
参数可以是:"r"
:只读(默认)。"w"
:写入(覆盖已有内容)。"a"
:追加(在文件末尾写入)。"rb"
/"wb"
/"ab"
:二进制模式(避免换行符转换)。
print(io.open("non-existent-file", "r")) --> nil non-existent-file: No such file or directory 2 print(io.open("/etc/passwd", "w")) --> nil /etc/passwd: Permission denied 13
assert(condition, error_message)
在 Lua 中用于错误处理,它接受两个参数——要检查的条件(通常是一个可能失败的函数调用)以及可选的错误信息,这在文件操作中特别有用。一种常见写法是:local f = assert(io.open(filename, mode))
这相当于:
local f, err = io.open(filename, mode) if not f thenerror(err, 2) -- 抛出错误,层级为2(显示调用assert的位置) end
assert
避免了繁琐的if not ... then error end
结构,一行代码同时完成文件打开和错误检查,成为 Lua 社区的惯用写法。不过如果需要更灵活的错误处理(如重试或降级方案),应该直接使用io.open
的返回值:local f, err = io.open(filename, mode) if not f then-- 自定义错误处理print("Warning:", err)-- 使用备用文件或默认值f = io.open(default_filename, mode) end
-
file:read(format)
:读取文件,与io.read()
类似,但作用在特定文件对象上。local f = assert(io.open(filename, mode)) local line = f:read("l") -- 读取一行 local num = f:read("n") -- 读取一个数字 local data = f:read(1024) -- 读取 1024 字节 f:close()
-
file:write(data1, data2, ...)
:写入数据到文件,类似io.write()
,但作用在特定文件对象上。f:write("Hello", " ", "World", "\n")
-
file:close()
:完整I/O模型必须手动关闭文件,否则可能导致数据丢失或资源泄漏。 -
io.input,io.output,io.stderr
:这是Lua 提供的三个预定义的标准流,允许混用完整I/O模型和简单I/O模型。调用无参数的io.input()
可以获得当前输入流,调用io.input(handle)
可以设置当前输入流,例如:-- 1. 临时改变当前输入流 local temp = io.input() -- 保存当前输入流 io.input("newinput") -- o打开一个新的输入流 do something with new input io.input():close() -- 关闭当前输入流 io.input(temp) -- 恢复之前的输入流-- 2. 将信息直接写到标准错误流中 io.stderr:write("Error: file not found!\n")
io.read(args)
实际上是io.input():read(args)
的简写,即函数read
是用在当前输入流上的。同样,io.write(args)
是io.output():write(args)
的简写。 -
io.lines
:以只读方式打开文件的输入流,并在到达文件末尾后关闭,其参数与io.read
一样。-- 以 8KB 为一个 block,将输入流复制到输出流中 for block in io.input():lines(2^13) doio.write(block) end
-
file:seek([whence][, offset])
:移动文件指针(类似 C 的fseek
),返回新位置相对于文件开头的偏移(字节单位),其参数如下:"set"
:相对于文件开头(默认)。"cur"
:相对于当前位置。"end"
:相对于文件末尾。offset
:偏移量(字节)。
-- 以下函数在不改变当前位置的情况下获取文件大小 function fsize (file)local current = file:seek() -- 保存当前位置local size = file:seek("end") -- 获取文件大小file:seek("set", current) -- 恢复当前位置-- file:seek("set", 0) -- 跳到文件开头return size end
-
file:setvbuf(mode[, size])
:设置文件流的缓冲方式,mode
参数可以是:"no"
:无缓冲(立即写入);"full"
:全缓冲(缓冲区满才写入),可设置缓冲区大小size;"line"
:行缓冲(遇到换行符才写入),可设置缓冲区大小size。
file:setvbuf("line") -- 行缓冲(适合交互式输出)
在大多数系统中,标准错误流(
io.stderr
)是不被缓冲的,而标准输出流(io.stdout
)按行缓冲。因此,当向标准输出中写入了不完整的行(例如进度条)时,可能需要刷新这个输出流才能看到输出结果。 -
file:flush()
:立即将缓冲区数据写入磁盘(类似 C 的fflush
)。file:write("Important data") file:flush() -- 确保数据写入磁盘
-
io.tmpfile()
:创建一个临时文,以读写模式打开,程序运行结束后自动删除该文件。local tmp = io.tmpfile() tmp:write("Temporary data") tmp:seek("set", 0) print(tmp:read("a")) -- 读取全部内容
7.2.2 典型应用
-
二进制文件复制
local src = assert(io.open("input.bin", "rb")) local dst = assert(io.open("output.bin", "wb")) while true dolocal bytes = src:read(4096) -- 4KB 块读取if not bytes then break enddst:write(bytes) end src:close() dst:close()
-
随机访问文件
local file = assert(io.open("data.bin", "rb")) file:seek("set", 1024) -- 跳到 1KB 位置 local chunk = file:read(256) -- 读取 256 字节 file:close()
7.3 OS库
本章节介绍 Lua 中与操作系统交互的几个关键函数。
-
os.rename
和os.remove
:分别表示文件重命名和删除文件。 -
os.exit([status [, close]])
:终止程序执行。status
可以是数字(0 表示成功)或布尔值(true
表示成功)。close
为可选参数,若设 为true
,会释放 Lua 状态占用的所有资源(这种终止方式通常是非必要的,因为大多数操作系统会在进程退出时释放其占用的所有资源)。
lua -- 进入Lua交互模式 os.exit() -- 退出Lua交互模式,或者按Ctrl+Z(windows系统)
-
os.getenv(varname)
:获取环境变量(字符串形式)的值 ,若变量未定义则返回nil
。print(os.getenv("HOME")) --> /home/lua
-
os.execute(command)
:执行系统命令(字符串形式),类似 C 的system
函数。
返回三个值:布尔值(成功与否)、字符串(终止类型:表示程序正常结束的"exit"
或因信号中断的"signal"
)、状态码(程序正常终结)或信号编号。 在POSIX和Windows中都可以使用如下的函数创建新目录:function createDir (dirname)os.execute("mkdir " .. dirname) end
-
io.popen(command [, mode])
:执行命令并与其输入/输出进行交互。它比os.execute
更灵活,因为它允许 Lua 脚本读取命令的输出或向命令发送输入, 其语法为:local file_handle = io.popen(command [, mode])
command
:要执行的系统命令(如 “dir /B” 或 “ls”)。mode
(可选):"r"
(默认):读取命令的输出(返回一个可读的文件句柄);或者是"w"
:写入数据到命令的输入(返回一个可写的文件句柄)。
-- 1.读取命令输出 -- 在 Windows 上使用 "dir /B",在 Linux/macOS 上使用 "ls" local f = io.popen("dir /B", "r") -- 打开命令并读取其输出 local dir = {} -- 存储结果的表-- 逐行读取命令输出并存入表 for entry in f:lines() dodir[#dir + 1] = entry endf:close() -- 关闭文件句柄
-- 2. 向命令写入数据 -- 通过系统命令 mail 发送一封电子邮件 local subject = "some news" local address = "someone@somewhere.org"-- 构造邮件命令(仅适用于 POSIX 系统,如 Linux/macOS) local cmd = string.format("mail -s '%s' '%s'", subject, address) local f = io.popen(cmd, "w") -- 打开命令并准备向其输入数据-- 写入邮件内容 f:write([[ Nothing important to say. -- me ]])f:close() -- 关闭文件句柄(发送邮件)
os.execute
和io.popen
的功能高度依赖操作系统,可能在某些环境中不可用。- 对于更复杂的操作系统功能(如目录操作),建议使用外部库(如 LuaFileSystem 或 luaposix)。
7.4 习题
-
请编写一个程序,该程序读取一个文本文件然后将每行的内容按照字母表顺序排序后重写该文件。如果在调用时不带参数,则从标准输入读取并向标准输出写入;如果在调用时传入一个文件名作为参数,则从该文件中读取并向标准输出写入;如果在调用时传入两个文件名作为参数,则从第一个文件读取并将结果写人到第二个文件中。
-
请改写上面的程序,使得当指定的输出文件已经存在时,要求用户进行确认。
local args = {...}local function readLines(input)local lines = {}for line in input:lines() dotable.insert(lines, line)endtable.sort(lines)return lines endlocal function writeLines(output, lines)for _, line in ipairs(lines) dooutput:write(line .. "\n")end endlocal function confirmOverwrite(filename)print(string.format("File '%s' already exists. Overwrite? (y/n): ", filename))local answer = io.read()return answer:lower() == "y" endif #args == 0 then-- Read from stdin, write to stdoutlocal lines = readLines(io.stdin)writeLines(io.stdout, lines) elseif #args == 1 then-- Read from file, write to stdoutlocal input = assert(io.open(args[1], "r"))local lines = readLines(input)writeLines(io.stdout, lines)input:close() elseif #args == 2 then-- Read from first file, write to second filelocal input = assert(io.open(args[1], "r"))local output = io.open(args[2], "r")if output thenoutput:close()if not confirmOverwrite(args[2]) thenprint("Operation cancelled.")returnendendoutput = assert(io.open(args[2], "w"))local lines = readLines(input)writeLines(output, lines)input:close()output:close() elseprint("Usage: lua sortlines.lua [inputfile [outputfile]]") end
-
练习7.3:对比使用下列几种不同的方式把标准输入流复制到标准输出流中的Lua程序的性能表现:
- 按字节
- 按行
- 按块(每个块大小为8KB)
- 一次性读取整个文件。对于这一种情况,输入文件最大支持多大?
local function copyByteByByte()local byte = io.read(1)while byte doio.write(byte)byte = io.read(1)end endlocal function copyLineByLine()local line = io.read("L")while line doio.write(line)line = io.read("L")end endlocal function copyInChunks()local chunk = io.read(2^13)while chunk doio.write(chunk)chunk = io.read(2^13)end endlocal function copyWholeFile()local content = io.read("a")io.write(content) end-- 测试性能 local function benchmark(func, name)local start = os.clock()func()local finish = os.clock()print(string.format("%s: %.2f seconds", name, finish - start)) endbenchmark(copyByteByByte, "Byte by byte") benchmark(copyLineByLine, "Line by line") benchmark(copyInChunks, "8KB chunks") benchmark(copyWholeFile, "Whole file")
-
请编写一个程序,该程序输出一个文本文件的最后一行。当文件较大且可以使用seek时,请尝试避免读取整个文件。
-
请将上面的程序修改得更加通用,使其可以输出一个文本文件的最后n行。同时,当文件较大且可以使用seek时,请尝试避免读取整个文件。
local function printLastNLines(filename, n)local file = assert(io.open(filename, "r"))-- 移动到文件末尾file:seek("end")local fileSize = file:seek()-- 从文件末尾开始逐行读取local lines = {}local position = fileSizewhile #lines < n and position > 0 dofile:seek("set", position)local line = file:read("L") -- 读取带换行符的一行if line thentable.insert(lines, 1, line) -- 在表的开头插入endposition = position - 1end-- 打印结果for i = 1, #lines doio.write(lines[i])endfile:close() end-- 测试代码 local filename = arg[1] local n = tonumber(arg[2] or 10) -- 默认打印最后 10 行 printLastNLines(filename, n)
-
使用
os.execute
执行系统命令来创建和删除目录,使用io.popen
读取目录内容function createDirectory(dirname)os.execute("mkdir " .. dirname) endfunction removeDirectory(dirname)os.execute("rmdir " .. dirname) endfunction collectDirectoryEntries(dirname)local entries = {}local f = io.popen("ls " .. dirname, "r")for entry in f:lines() dotable.insert(entries, entry)endf:close()return entries end-- 测试代码 createDirectory("testdir") removeDirectory("testdir") local entries = collectDirectoryEntries(".") for _, entry in ipairs(entries) doprint(entry) end
第八章:控制结构
8.1 变量
8.1.1 局部变量 VS 全局变量
在Lua中,变量默认是全局的,要创建局部变量(Local Variables),必须显式使用local
关键字声明。局部变量的作用域仅限于声明它的块(Blocks) 内。块可以是:
- 函数体;
- 整个文件或字符串(称为chunk);
- 显式的
do-end
块; - 控制结构的主体,如if-then、while、for等。
注意:if-then-else的结构中,then块和else块是两个独立的作用域,即在then块中声明的local x 不会影响else块。
x = 10 -- 全局变量,存储在全局环境表 _G 中(即 _G.x = 10)
local i = 1 -- 局部变量,从声明处开始,到当前代码块(chunk)结束while i <= x dolocal x = i * 2 -- 局部变量,作用域是while循环体print(x) -- 循环内访问的是局部x,输出2,4,6,8...i = i + 1
end -- 作用域结束,局部x被销毁if i > 20 thenlocal x -- 局部变量,作用域是if语句x = 20 -- 访问的是局部xprint(x + 2) -- 作用域结束,局部x被销毁
elseprint(x) -- 输出全局x的值10
endprint(x) -- 输出全局x的值10
上述代码中,第一个local i = 1
的作用域是整个代码块,如果整个代码块写在一个文件内,么其作用域就是整个文件,但它还是一个局部变量,和全局变量x还是有明显的区别:
-
全局变量:
- 存储在全局表中,作用域从创建处开始,直到程序结束;
所有全局变量默认存储在名为
_G
的全局表中(本质上只是_G
表的字段),不同Lua文件(模块)共享同一个_G
表。每次访问都要查全局变量表_G
,速度较慢。- 可以跨函数、跨文件访问;
- 生命周期持续到程序结束(或手动置为
nil
) - 命名规范:全大写+下划线(如
APP_CONFIG
),或者是添加命名前缀(如GS_
表示游戏状态)
-- 其他文件也可以访问: print(_G.x) -- 输出 10 -- 其他文件可能修改: _G.x = 999 -- 你的代码中的 `x` 突然被改了
-
局部变量:
- 存储在局部变量栈帧中,访问时直接操作栈内存,速度更快;
- 作用域仅限于声明它的块(Blocks) 内,用完自动回收。
-- 其他文件无法访问: print(i) -- 报错:`i` 未定义(除非其他文件也声明了 `i`)
特性 | 全局变量x | 局部变量x |
---|---|---|
示例 | x = 10 | local x = 10 |
存储位置 | 全局表 _G中,访问较慢(需查表) | 局部变量栈帧中,访问较快(栈访问) |
作用域 | 整个程序(可以跨函数、跨文件访问) | 仅限于声明它的块内 |
生命周期 | 永久 | 块执行期间 |
内存管理 | 需手动置nil | 自动回收 |
安全性 | 跨模块共享数据,可能被其他代码修改 | 当前模块内部使用,不会被外部修改 |
推荐使用场景 | 配置项、全局状态 | 临时变量、函数内部变量 |
变量遮蔽(Variable Shadowing):上述代码中,while
循环内的local x
遮蔽了全局的x
,当局部变量离开作用域后,同名的全局变量又恢复可见。这是因为Lua查找变量时遵循"就近原则":
- 先在当前块查找局部变量;
- 然后向外层作用域查找
- 最后查找全局表_G
if i > 20 thenlocal x -- 作用域仅限于这个then块x = 20 -- 修改的是局部xprint(x + 2) -- 访问局部x
else-- 这里没有声明local x,所以查找顺序:-- 1. 当前else块 → 无-- 2. 外层chunk → 找到全局xprint(x) -- 访问全局x
end
8.1.2 局部变量
以上代码在交互模式下不能正常运行,因为在交互模式下,每行代码被视为独立的chunk,这会导致局部变量立即失效。解决方法是用do-end
显式创建块。一旦输入了do,命令就只会在遇到匹配的end时才结束,这样Lua语言解释器就不会单独执行每一行的命令:
dolocal i = 1-- 这里i仍然有效
end
当需要精确控制某些局部变量的生效范围时,do
程序块也同样有用:
local x1, x2
dolocal a2 = 2*alocal d = (b^2 - 4*a*c)^(1/2)x1 = (-b + d)/a2x2 = (-b - d)/a2
end -- 'a2' and 'd' 的有效范围在此结束
print(x1, x2) -- 'x1' and 'x2' 仍在范围内
变量使用原则:所有变量首先考虑用local,必须全局时才提升作用域。局部变量的优势有:
- 避免污染全局命名空间
- 避免同一程序中不同代码部分的命名冲命
- 局部变量比全局变量访问速度更快
- 局部变量随着作用域的结束而自动消失,便于垃圾回收
与常见的多重赋值规则一样,局部变量的声明可以包含初始值,多余的变量被赋值为nil
。如果没有初始赋值,则变量会被初始化为nil
:
local a, b = 1, 10
if a < b thenprint(a) --> 1local a -- '= nil' 是隐式的print(a) --> nil
end -- 'then'代码块在此结束
print(a, b) --> 1 10
Lua 语言中有一种常见的用法——创建一个局部变量 foo。并将将全局变量foo的值赋给局部变量foo。
local foo = foo --
这种用法可以:
- 加速访问:Lua 访问全局变量需要查表(全局环境表 _G),而访问局部变量是直接操作栈,速度更快。将全局变量缓存为局部变量后,后续访问会快很多,特别是在循环中多次使用时;
- 防止后续代码修改全局foo,影响当前逻辑,尤其是在运行时动态修改模块或类的行为时。
比如通过local print = print
这样的声明,即使后续代码修改了全局的print
函数,你的局部变量仍然指向原始的print
函数。这在开发库代码时特别有用,可以确保你的代码使用预期的函数版本
8.2 控制结构
Lua 语言提供了一组精简且常用的控制结构(control structure ),包括用于条件执行的if
以及用于循环的while
、repeat
和for
。所有的控制结构语法上都有一个显式的终结符:end
,用于终结if
、for
及while
结构,until
用于终结repeat
结构。
8.2.1 if 条件语句
Lua的条件语句包括if-then-else
(else
部分可选)和elseif
结构。在Lua中,条件表达式的结果可以是任何值,但只有false
和nil
被视为假,其他值(包括0和空字符串)都视为真。
-- 简单if
if a < 0 then a = 0 end-- if-then-else
if a < b then return a else return b end
如果要编写嵌套的if
语句,可以使用elseif
。它类似于在else
后面紧跟一个if
,但可以避免重复使用end
:
-- 多条件elseif
if op == "+" thenr = a + b
elseif op == "-" thenr = a - b
elseif op == "*" thenr = a * b
elseif op == "/" thenr = a / b
elseerror("invalid operation")
end
由于Lua语言不支持
switch
语句,所以这种一连串的else-if
语句比较常见。
8.2.2 循环语句
8.2.2.1 while
循环
顾名思义,当条件为真时while
循环会重复执行其循环体(先检查条件)
local i = 1
while a[i] doprint(a[i])i = i + 1
end
8.2.2.2 repeat-until
循环
先执行循环体,再检查条件,所以循环体至少会执行一次。
-- 1. 读取第一个非空行
local line
repeatline = io.read()
until line ~= ""
print(line)
和大多数其他编程语言不同,在Lua语言中,循环体内声明的局部变量的作用域包括测试条件:
-- 2. 牛顿法求平方根
local sqrt = x / 2
repeatsqrt = (sqrt + x/sqrt) / 2local error = math.abs(sqrt^2 - x)
until error < x/10000 -- error在这里仍然可见
8.2.2.3 for
循环
Lua有两种for
循环:数值型(numerical)for
和泛型(generic)for
。数值for的语法如下:
for var = exp1, exp2, exp3 do-- 循环体
end
初始化阶段,计算 exp1
(起始值), exp2
(结束值,包含该值), exp3
(步长,可选,默认为1
) 的值,将 局部变量var
初始化为 exp1
。每次迭代前,检查是否满足继续循环的条件:
- 如果
exp3 > 0
:检查是否var <= exp2
- 如果
exp3 < 0
:检查是否var >= exp2
每次迭代后,执行var = var + exp3
,所以要在循环内修改 var
的值是无效的,因为每次迭代会被覆盖,可以使用 break
来提前终止循环。例如:
-- 从10递减到1,步长-2
for i = 10, 0, -2 doprint(i) -- 输出10,8,6,4,2,0
end
控制变量var
在循环结束后自动销毁,如果需要在循环结束后使用var
的值(通常在中断循环时),则必须将其保存到另一个变量中:
-- 在列表a中找到第一个值为负数的元素,返回其位置
local found = nil
for i = 1, #a doif a[i] < 0 thenfound = i -- 保存 'i'的值break -- 找到第一个解后立即退出end
end
print(found) -- 68
如果不想给循环设置上限,可以使用常量math.huge
。math.huge
是 Lua 提供的一个特殊常数,表示浮点数的正无穷大,等同于 IEEE 754 标准中的 inf
。
print(math.huge) -- 输出:inf
print(1e308 < math.huge) -- 输出:true
print(math.huge + 1) -- 输出:inf
-- 无上限循环,寻找满足不等式 0.3*i³ - 20*i² - 500 ≥ 0 的最小正整数 i
for i = 1, math.huge doif (0.3*i^3 - 20*i^2 - 500 >= 0) thenprint(i)breakend
end
泛型for用于遍历迭代器返回的值,如pairs,ipairs,io.lines
等,比如:
-- 1. 遍历字典
for k, v in pairs(t) doprint(k, v)
end
以下代码只遍历从 1 开始的连续整数索引,遇到 nil 会停止,所以不输出 [5] = “black”,性能比 pairs 更高(针对纯数组)。
-- 2. 遍历数组(连续数字索引部分)
local colors = {"red", "green", "blue", [5] = "black"}for index, value in ipairs(colors) doprint(index, value)
end
逐行读取文件:
local file = io.open("data.txt")
for line in file:lines() do -- 显式文件对象方式-- 处理行内容
end
file:close()
8.3 跳转语句
8.3.1 break和return
break
:退出当前循环(包含它的内层循环,如for 、repeat或者while)return
:返回函数的执行结果或简单地结束函数的运行
local i = 1
while a[i] do -- 检查a[i]是否存在(非nil)if a[i] == v then return i end -- 如果找到目标值v,直接返回当前索引,终止整个函数i = i + 1 -- 否则检查下一个位置
end
Lua 的设计要求 return
必须是 end/else/until
前的最后一条语句,或者是代码块的最后一条语句,确保代码逻辑清晰,避免意外行为(return
后的部分不会被执行)。
function foo()return -- 语法错误:后面还有其它语句-- 这里永远不会执行的代码print("这行永远不会执行")
end
通过在 return
外包裹 do-end
块,可以合法地提前返回:
function foo()do return end -- 合法:return 现在是 do-end 块的最后一个语句print("这行永远不会执行")
end
所以正常情况下,遵循标准模式,将 return 放在函数末尾;调试时,可以临时用 do return end 跳过代码:
function complex_calculation()-- 调试时临时跳过后续代码do return mock_result end -- 加在这里测试-- 实际复杂的计算过程local result = ...-- 更多处理...return result
end
8.3.2 goto语句
8.3.2.1 基础用法
goto 语句:跳转到指定的标签位置执行代码,其语法为:
goto label -- 跳转到标签::label:: -- 标签定义,这种语法是为了在代码中突出显示标签
-- 代码块
限制:
-
不能跳进代码块(如
if
、for
、while
等内部,以及函数内部)。 -
不能跳出函数(即不能从函数 A 跳转到函数 B)。
-
不能跳进局部变量的作用域(避免变量未初始化问题)。
在 Lua 中,局部变量的作用域从其声明点开始,到包含它的块的最后一条非空语句结束。标签(::label::
)被视为空语句(void statements),不影响变量作用域。例如:-- var 的作用域从声明处开始,到 some code 结束(代码块中最后一个非空语句) -- ::continue:: 标签被视为空语句,因此 goto continue 并没有跳入 var 的作用域 while some_condition doif some_other_condition then goto continue endlocal var = something -- 局部变量声明some code -- 非空语句::continue:: -- 标签(空语句) end
将标签视为空语句,不影响变量作用域边界,这种设计确保 goto 不会意外跳入变量作用域,使得控制流跳转更加安全可控。
goto
的典型用途,可参考其他一些编程语言中存在但Lua语言中不存在的代码结构,例如continue
、多级break
、多级continue
、redo
和局部错误处理(try
语句)等。
-
continue
语句:使用跳转到位于循环体最后位置处标签的goto 语句,跳过当前循环的剩余部分 -
redo
语句:通过跳转到代码块开始位置的goto语句来完成while some_condition do::redo::if some_other_condition then goto continueelse if yet_another_condition then goto redoendsome code::continue:: end
-
跳出多层嵌套循环:Lua 没有提供直接跳出多层循环的语句,goto 可以解决这个问题:
for i = 1, 10 dofor j = 1, 10 doif some_condition(i, j) thengoto break_all_loopsendend end::break_all_loops:: print("Loop exited")
-
实现状态机:
-- 1. 检查输入中 0 的个数是否为偶数::s1:: dolocal c = io.read(1)if c == '0' then goto s2elseif c == nil then print'ok'; returnelse goto s1end end -- 交替跳转 s1 和 s2,如果结束时在 s1 则说明 0 的个数是偶数。 ::s2:: dolocal c = io.read(1)if c == '0' then goto s1elseif c == nil then print'not ok'; returnelse goto s2end end
-- 2. 迷宫游戏:每个房间是一个状态,goto 实现状态切换 goto room1 -- 初始房间::room1:: dolocal move = io.read()if move == "south" then goto room3elseif move == "east" then goto room2elseprint("invalid move")goto room1 -- 无效输入时跳转回当前房间end end::room2:: dolocal move = io.read()if move == "south" then goto room4elseif move == "west" then goto room1elseprint("invalid move")goto room2end end::room3:: dolocal move = io.read()if move == "north" then goto room1elseif move == "east" then goto room4elseprint("invalid move")goto room3end end::room4:: doprint("Congratulations, you won!") end
该游戏由4 个房间组成,形成 2x2 的网格,玩家通过输入方向指令(north/south/east/west)在房间间移动,到达 room4 时游戏胜利
room1 (西,北) -- east --> room2| south | southv v room3 -- east --> room4 (终点)
何时应该使用 goto:
- 当它能显著简化代码结构时
- 当没有更好的替代方案时(如多层循环跳出)
- 在性能关键代码中,经测试确实能带来提升时
在大多数情况下,Lua 的函数、循环和条件语句已经足够表达各种控制流,goto 应该作为最后的选择。
8.3.2.2 局限性
在迷宫游戏中,虽然 goto 可以正常工作,但通常更好的做法是:
-
使用状态变量表示当前房间
-
使用函数封装每个房间的逻辑
-
使用表结构存储房间连接关系
goto
虽然在某些情况下可以使用,但通常会导致代码难以维护和扩展,以下是几种更结构化的替代方案:
-- 1. 使用状态变量 + 循环
local current_room = "room1"while true doif current_room == "room1" then print("You are in room 1. Exits: south, east")local move = io.read()if move == "south" then current_room = "room3"elseif move == "east" then current_room = "room2"else print("invalid move") endelseif current_room == "room2" then print("You are in room 2. Exits: south, west")local move = io.read()if move == "south" then current_room = "room4"elseif move == "west" then current_room = "room1"else print("invalid move") endelseif current_room == "room3" then print("You are in room 3. Exits: north, east")local move = io.read()if move == "north" then current_room = "room1"elseif move == "east" then current_room = "room4"else print("invalid move") endelseif current_room == "room4" then print("Congratulations, you won!")breakend
end
-- 2. 使用表结构 + 函数封装
-- 修正后的表结构方案(最佳平衡)
local function get_keys(t)local keys = {}for k in pairs(t) do table.insert(keys, k) endreturn keys
endlocal rooms = {room1 = {description = "You are in room 1",exits = { south = "room3", east = "room2" }},room2 = {description = "You are in room 2",exits = { south = "room4", west = "room1" }},room3 = {description = "You are in room 3",exits = { north = "room1", east = "room4" }},room4 = {description = "Congratulations, you won!",exits = {} -- 标记为终点}
}local current_room = "room1"while true dolocal room = rooms[current_room]print(room.description)-- 检查终点if not next(room.exits) then break end print("Exits: " .. table.concat(get_keys(room.exits), ", "))local move = io.read()if room.exits[move] thencurrent_room = room.exits[move]elseprint("Invalid move! Try again.")end
end
-- 3. 面向对象风格
-- 辅助函数:获取表的键
local function get_keys(t)local keys = {}for k in pairs(t) dotable.insert(keys, k)endreturn keys
end-- 定义房间类
local Room = {new = function(self, description, exits)local room = {description = description,exits = exits or {} -- 默认为空表}setmetatable(room, self)self.__index = selfreturn roomend,enter = function(self)print(self.description)-- 检查是否有出口if not next(self.exits) thenreturn false -- 游戏结束end-- 显示可用出口local exit_list = get_keys(self.exits)print("Exits: " .. table.concat(exit_list, ", "))return trueend
}-- 创建房间实例
local room1 = Room:new("You are in a dimly lit room. Dust covers the floor.", {south = "room3", east = "room2"})local room2 = Room:new("This room has a strange glowing crystal in the corner.", {south = "room4", west = "room1"})local room3 = Room:new("You hear dripping water in this damp, cold chamber.", {north = "room1", east = "room4"})local room4 = Room:new("Congratulations! You found the treasure room!")-- 房间映射表
local rooms = {room1 = room1,room2 = room2,room3 = room3,room4 = room4}-- 游戏主循环
local current_room = "room1"print("=== Welcome to the Dungeon Explorer! ===")
print("Navigate using directions: north, south, east, west\n")while true dolocal room = rooms[current_room]-- 进入房间并检查游戏是否结束if not room:enter() thenbreakend-- 获取玩家移动指令io.write("> ")local move = io.read():lower()-- 处理移动指令if room.exits[move] thencurrent_room = room.exits[move]elseprint("Invalid move! Try again.")end
endprint("\nGame over. Thanks for playing!")
方案 | 优点 | 缺点 |
---|---|---|
状态变量 | 简单直观 | 房间逻辑混在一起,扩展性差 |
表结构 | 数据与逻辑分离,易于扩展 | 需要额外的数据结构支持 |
面向对象 | 封装性好,最易维护和扩展 | 需要理解元表和OOP模式 |
对于小型游戏,方案2(表结构)通常是平衡简单性和扩展性的最佳选择。随着游戏复杂度增加,方案3(面向对象)会显示出更大优势。
8.4 练习题解答
-
Lua中四种无条件循环的实现方式:
- while true循环:
while true do-- 循环体if condition then break end end
- repeat-until false循环:
repeat-- 循环体if condition then break end until false
- 数值for循环:
for i = 1, math.huge do-- 循环体if condition then break end end
- goto循环:
::loop::-- 循环体if not condition then goto loop end
个人偏好:
while true
最直观且常用,性能也较好。 -
用尾调用重新实现迷宫游戏:
function room1()local move = io.read()if move == "south" then return room3()elseif move == "east" then return room2()elseprint("invalid move")return room1()end endfunction room2()local move = io.read()if move == "south" then return room4()elseif move == "west" then return room1()elseprint("invalid move")return room2()end endfunction room3()local move = io.read()if move == "north" then return room1()elseif move == "east" then return room4()elseprint("invalid move")return room3()end endfunction room4()print("Congratulations, you won!")-- 游戏结束,不需要返回 end-- 开始游戏 room1()
-
Lua限制goto不能跳出函数的原因:
- 实现复杂性:函数调用涉及调用栈管理,goto跳出函数会破坏栈结构
- 变量作用域:函数内局部变量的生命周期与函数调用相关,跳出函数会导致变量访问问题
- 资源管理:难以确保函数内分配的资源能正确释放
- 代码可读性:跨函数跳转会大大降低代码可读性和可维护性
- 异常处理:Lua没有完善的异常处理机制,这种跳转可能导致不可预知的行为
-
假设goto可以跳出函数,下面的程序会如何执行:
function getLabel()return function() goto L1 end::L1::return 0 endfunction f(n)if n == 0 then return getLabel()elselocal res = f(n - 1)print(n)return resend endx = f(10) x()
执行过程:
f(10)
递归调用到f(0)
,返回getLabel()
的结果getLabel()
返回一个匿名函数,该函数包含goto L1
x()
执行这个匿名函数,尝试跳转到L1
这个例子展示了为什么goto
不能跳出函数 - 它会导致不可预测的行为和潜在的安全问题: 如果允许跳出函数,goto
会尝试跳转到getLabel
函数中的L1
标签,但getLabel
函数已经执行完毕,其栈帧已销毁,这会导致未定义行为,可能访问无效内存或抛出错误。