此前,我们已经介绍过函数 dofile
,它是运行 Lua
代码的主要方式之一。实际上,函数 dofile
是一个辅助函数,函数 loadfile
才完成了真正的核心工作。与函数 dofile
类似,函数 loadfile
也是从文件中加载 Lua
代码段,但它不会运行代码,而只是编译代码,然后将编译后的代码段作为一个函数返回。此外,与函数 dofile
不同,函数 loadfile
只返回错误码而不抛出异常。可以认为,函数 dofile
就是:
function dofile (filename) local f = assert(loadfile(filename)) return f() end
注意
如果函数 loadfile
执行失败,那么函数 assert
会引发一个错误。
对于简单的需求而言,由于函数 dofile
在一次调用中就做完了所有工作,所以函数非常易用。不过,函数 loadfile
更灵活。在发生错误的情况中,函数 loadfile
会返回 nil
及错误信息,以允许我们按自定义的方式来处理错误。此外,如果需要多次运行同一个文件,那么只需要用一次 loadfile
函数后再次调用它的返回结果即可。由于只编译一次文件,因此这种方式的开销要不多次调用函数 dofile
小得多(编译在某种程度上相比其他操作开销更大)。
函数 load
与函数 loadfile
类似,不同之处在于该函数从一个字符串或函数中读取代码段,而不是从文件中读取。
提示
在 Lua5.1
中,函数 loadstring
用于完成 load
所完成的从字符串中加载代码的功能。
例如,考虑如下代码:
f = load("i = i + 1")
在这段代码执行后,变量 f
就会变成一个被调用执行 i = i + 1
的函数:
i = 0 f(); print(i) --> 1 f(); print(i) --> 2
注意
尽管函数 load
的功能很强大,但还是应该谨慎地使用。相对于其他可选的函数而言,该函数的开销较大并且可能会引起诡异的问题。请先确认当下已经找不到更简单的解决方式后再使用该函数。
如果要编写一个用后即弃的 dostring
函数(例如加载并运行一段代码),那么我们可以直接调用函数 load
的返回值:
load(s)()
不过,如果代码中有语法错误,函数 load
就会返回 nil
和形如“**试图调用一个nil值(attempt to call a nil value)**”的错误信息。为了更清楚地展示错误信息,最好使用函数 assert
:
assert(load(s))()
通常,用函数 load
来加载字符串常量是没有意义的。例如,如下的两行代码基本等价:
f = load("i = i + 1") f = function () i = i + 1 end
但是,由于第二行代码会与其外层的函数一起被编译,所以其执行速度要快得多。与之对比,第一段代码在调用函数 load
时会进行一次独立的编译。
由于函数 load
在编译时不涉及词法定界,所以上述示例的两端代码可能并不完全等价。为了清晰地展示他们之间的区别,让我们稍微修改以下上面的例子:
i = 32 local i = 0 f = load("i = i + 1; print(i)") g = function () i = i + 1; print(i) end f() --> 33 g() --> 1
函数 g
像我们所预期地那样操作局部变量 i
,但函数 f
操作的却是全局变量 i
,这是由于函数 load
总是在全局环境中编译代码段。
函数 load
最典型的用法是执行外部代码(即那些来自程序本身之外的代码段)或动态生成的代码。例如,我们可能想运行用户定义的函数,由用户输入函数的代码后调用函数 load
对其求值。
注意
函数 load
期望的输入是一段程序,也就是一系列语句。如果需要对表达式求值,那么可以在表达式前添加 return
,这样才能构成一条返回指定表达式值的语句。
print "enter your expression:" local line = io.read() local func = assert(load("return "..line)) print("the value of your expression is "..func())
由于函数 load
所返回的函数就是一个普通函数,因此可以反复对其进行调用:
print "enter function to be plotted (with variable 'x'):" local line = io.read() local f = assert(load("return "..line)) for i = 1, 20 do x = i -- 全局的'x'(当前代码段内可见) print(string.rep("*", f())) end
我们也可以使用读取函数作为函数 load
的第一个参数。读取函数可以分几次返回一段程序,函数 load
会不断地调用读取函数知道读取函数返回 nil
(表示程序段结束)。作为示例,以下的调用与函数 loadfile
等价:
f = load(io.lines(filename, "*L"))
调用 io.lines(filename, "*L")
返回一个函数,这个函数每次被调用时就从指定文件返回一行。因此,函数 load
会一行一行地从文件中读出一段程序。以下的版本与之相似但效率稍高:
f = load(io.lines(filename, 1024))
这里,函数 io.lines
返回的迭代器会以 1024
字节为块读取源文件。
Lua
语言将所有独立的代码段当做匿名可变长参数函数的函数体。例如, load("a = 1")
的返回值与以下表达式等价:
function (...) a = 1 end
像其他任何函数一样,代码段中可以声明局部变量:
f = load("local a = 10; print(a + 20)") f() --> 30
使用这个特性,可以在不使用全局变量 x
的情况下重写之前运行用户定义函数的示例:
print "enter function to be plotted (with variable 'x'):" local line = io.read() local f = assert(load("local x = ...; return " .. line)) for i = 1, 20 do print(string.rep("*", f(i))) end
在上述代码中,在代码段开头增加了 "local x = ..."
来将 x
声明为局部变量。之后使用参数 i
调用函数 f
,参数 i
就是可变长参数表达式的值( ...
)。
函数 load
和函数 loadfile
从来不引发错误。当有错误发生时,他们会返回 nil
及错误信息:
print(load("i i")) --> nil [string "i i"]:1: '=' expected near 'i'点击复制复制失败已复制
此外,这些函数没有任何副作用,他们既不改变或创建变量,也不向文件写入等。这些函数只是将程序段编译为一种中间形式,然后将结果作为匿名函数返回。一种常见的误解是认为加载一段程序也就是定义了函数,但实际上在 Lua
语言中函数定义是在运行时而不是在编译时发生的一种赋值操作。例如,假设有一个文件 foo.lua
:
-- 文件'foo.lua' function foo(x) print(x) end
当执行:
f = loadfile("foo.lua")
时,编译 foo
的命令并没有定义 foo
,只有运行代码才会定义它:
f = loadfile("foo.lua") print(foo) --> nil f() -- 运行代码 foo("ok") --> ok
这种行为可能看上去有些奇怪,但如果不使用语法糖对其进行重写则看上去会清晰很多:
-- 文件'foo.lua' foo = function (x) print(x) end
如果线上产品级别的程序需要执行外部代码,那么应该处理加载程序段时报告的所有错误。此外,为了避免不愉快的副作用发生,可能还应该在一个受保护的环境中执行这些代码。