Lua -<5>- 函数

  • 注:lua5.0以上版本去除了table.getn方法,可以用#tableName代替
  • 函数有两种用途:1.完成指定的任务,这种情况下函数作为调用语句使用;2.计算并返回值,这种情况下函数作为赋值语句的表达式使用。
    语法:
function func_name (arguments-list) 
 statements-list; 
end; 

调用函数的时候,如果参数列表为空,必须使用()表明是函数调用。

print(8*9, 9/8) 
a = math.sin(3) + math.cos(10) 
print(os.date())

上述规则有一个例外,当函数只有一个参数并且这个参数是字符串或者表构造的时候,()可有可无:

print "Hello World" <--> print("Hello World") 
dofile 'a.lua' <--> dofile ('a.lua') 
print [[a multi-line message]]<--> print([[a multi-line message]]) 
f{x=10, y=20} <--> f({x=10, y=20}) 
type{}

Lua 也提供了面向对象方式调用函数的语法,比如 o:foo(x)与 o.foo(o, x)是等价的

Lua 使用的函数,既可是 Lua 编写的,也可以是其他语言编写的,对于 Lua 程序员,用什么语言实现的函数使用起来都一样。
Lua 函数实参和形参的匹配与赋值语句类似,多余部分被忽略,缺少部分用 nil 补足。

function f(a, b) return a or b end
CALL PARAMETERS 
f(3) a=3, b=nil 
f(3, 4) a=3, b=4 
f(3, 4, 5) a=3, b=4 (5 is discarded) 
18.png
  • 多返回值
    Lua 函数可以返回多个结果值,比如 string.find,其返回匹配串“开始和结束的下标”(如果不存在匹配串返回 nil)。
s, e = string.find("hello Lua users", "Lua") 
print(s, e) --> 7 9 -- Lua 是从第 7 个索引找到的

Lua 函数中,在 return 后列出要返回的值得列表即可返回多值,如:

function maximum (a) 
local mi = 1 -- maximum index 
local m = a[mi] -- maximum value 
for i,val in ipairs(a) do
 if val > m then
 mi = i 
 m = val 
 end 
end 
return m, mi 
end 
print(maximum({8,10,23,12,5})) --> 23 3 

Lua 总是调整函数返回值的个数以适用调用环境,当作为独立的语句调用函数时,所有返回值将被忽略。假设有如下三个函数:

function foo0 () end -- returns no results 
function foo1 () return 'a' end -- returns 1 result 
function foo2 () return 'a','b' end -- returns 2 results
  • 第一,当作为表达式调用函数时,有以下几种情况:
      1. 当调用作为表达式最后一个参数或者仅有一个参数时,根据变量个数函数尽可能多地返回多个值,不足补 nil,超出舍去。
      1. 其他情况下,函数调用仅返回第一个值(如果没有返回值为 nil)
-- 接上个方法
x,y = foo2() -- x='a', y='b' 
x = foo2() -- x='a', 'b' is discarded 
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(), 20 -- x='a', y=20 
x,y = foo0(), 20, 30 -- x='nil', y=20, 30 is discarded 
  • 第二,函数调用作为函数参数被调用时,和多值赋值是相同。
print(foo0()) --> 
print(foo1()) --> a 
print(foo2()) --> a b 
print(foo2(), 1) --> a 1 
print(foo2() .. "x") --> ax 
  • 第三,函数调用在表构造函数中初始化时,和多值赋值时相同。
a = {foo0()} -- a = {} (an empty table) 
a = {foo1()} -- a = {'a'} 
a = {foo2()} -- a = {'a', 'b'} 
a = {foo0(), foo2(), 4} -- a[1] = nil, a[2] = 'a', a[3] = 4

另外,return f()这种形式,则返回“f()的返回值”:

function foo (i) 
    if i == 0 then 
        return foo0() 
    elseif i == 1 then 
        return foo1() 
    elseif i == 2 then 
        return foo2() 
    end 
end 
print(foo(1)) --> a 
print(foo(2)) --> a b 
print(foo(0)) -- (no results) 
print(foo(3)) -- (no results) 

可以使用圆括号强制使调用返回一个值。

print((foo0())) --> nil 
print((foo1())) --> a 
print((foo2())) --> a 
291.png

一个 return 语句如果使用圆括号将返回值括起来也将导致返回一个值。


15281798113170.png

15281798342641.png
  • 函数多值返回的特殊函数 unpack,接受一个数组作为输入参数,返回数组的所有元素。unpack 被用来实现范型调用机制,在 C 语言中可以使用函数指针调用可变的函数,可以声明参数可变的函数,但不能两者同时可变。在 Lua 中如果你想调用可变参数的可变函数只需要这样:
f(unpack(a)) 

unpack 返回 a 所有的元素作为 f()的参数:

f = string.find 
a = {"hello", "ll"} 
print(f(unpack(a))) --> 3 4 

(ps:unpack它接受一个数组(table)作为参数,并默认从下标1开始返回数组的所有元素。

local info={1,2,3,4,5,6}
local a,b,c,d,e,f=unpack(info)
print(a,b,c,d,e,f)
输出结果:1   2   3   4   5   6

如果遇到数组下有nil,或者有字母key的时候:

local info={1,2,3,nil,5,p=6}
local a,b,c,d,e,f=unpack(info)
print(a,b,c,d,e,f)
输出结果:1   2   3   nil    5   nil

)
预定义的 unpack 函数是用 C 语言实现的,我们也可以用 Lua 来完成:

function unpack(t, i) 
 i = i or 1 
if t[i] then
 return t[i], unpack(t, i + 1) 
end 
end 
  • 可变参数
    Lua 函数可以接受可变数目的参数,和 C 语言类似在函数参数列表中使用三点(...)表示函数有可变的参数。Lua 将函数的参数放在一个叫 arg 的表中,除了参数以外,arg表中还有一个域 n 表示参数的个数。
    例如,我们可以重写 print 函数:
printResult = ""
function print(...) 
for i,v in ipairs(arg) do
 printResult = printResult .. tostring(v) .. "\t"
end 
 printResult = printResult .. "\n"
end 

有时候我们可能需要几个固定参数加上可变参数

function g (a, b, ...) end
CALL PARAMETERS 
g(3) a=3, b=nil, arg={n=0} 
g(3, 4) a=3, b=4, arg={n=0} 
g(3, 4, 5, 8) a=3, b=4, arg={5, 8; n=2} 

如上面所示,Lua 会将前面的实参传给函数的固定参数,后面的实参放在 arg 表中。举个具体的例子,如果我们只想要 string.find 返回的第二个值。一个典型的方法是使用哑元(dummy variable,下划线):

local _, x = string.find(s, p) 
-- now use `x' 
... 

还可以利用可变参数声明一个 select 函数:

function select (n, ...) 
return arg[n] 
end 
print(string.find("hello hello", " hel")) --> 6 9 
print(select(1, string.find("hello hello", " hel"))) --> 6 
print(select(2, string.find("hello hello", " hel"))) --> 9 
825569106.png

有时候需要将函数的可变参数传递给另外的函数调用,可以使用前面我们说过的unpack(arg)返回 arg 表所有的可变参数,Lua 提供了一个文本格式化的函数 string.format(类似 C 语言的 sprintf 函数):

function fwrite(fmt, ...) 
return io.write(string.format(fmt, unpack(arg))) 
end 
  • 命名参数
    Lua 的函数参数是和位置相关的,调用时实参会按顺序依次传给形参。有时候用名字指定参数是很有用的,比如 rename 函数用来给一个文件重命名,有时候我们我们记不清命名前后两个参数的顺序了:
-- invalid code 
rename(old="temp.lua", new="temp1.lua") 

上面这段代码是无效的,Lua 可以通过将所有的参数放在一个表中,把表作为函数的唯一参数来实现上面这段伪代码的功能。因为 Lua 语法支持函数调用时实参可以是表的构造。
rename{old="temp.lua", new="temp1.lua"}
根据这个想法我们重定义了 rename:

function rename (arg)
return os.rename(arg.old, arg.new) 
end

当函数的参数很多的时候,这种函数参数的传递方式很方便的。例如 GUI 库中创建窗体的函数有很多参数并且大部分参数是可选的,可以用下面这种方式:

w = Window { 
 x=0, y=0, width=300, height=200, 
 title = "Lua", background="blue", 
 border = true
} 
function Window (options) 
-- check mandatory options 
if type(options.title) ~= "string" then
 error("no title") 
elseif type(options.width) ~= "number" then
 error("no width") 
elseif type(options.height) ~= "number" then
 error("no height") 
end 
-- everything else is optional 
 _Window(options.title, 
 options.x or 0, -- default value 
 options.y or 0, -- default value 
 options.width, options.height, 
 options.background or "white", -- default 
 options.border -- default is false (nil) 
 ) 
end 
再论函数

Lua 中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。
第一类值指:在 Lua 中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。
词法定界指:嵌套的函数可以访问他外部函数中的变量。这一特性给 Lua 提供了强大的编程能力。
Lua 中关于函数稍微难以理解的是函数也可以没有名字,匿名的。当我们提到函数名(比如 print),实际上是说一个指向函数的变量,像持有其他类型值的变量一样:

a = {p = print} 
a.p("Hello World") --> Hello World 
print = math.sin -- `print' now refers to the sine function 
a.p(print(1)) --> 0.841470 
sin = a.p -- `sin' now refers to the print function 
sin(10, 20) --> 10 20 

既然函数是值,那么表达式也可以创建函数了,Lua 中我们经常这样写:
function foo (x) return 2*x end
这实际上是 Lua 语法的特例,下面是原本的函数:
foo = function (x) return 2*x end
函数定义实际上是一个赋值语句,将类型为 function 的变量赋给一个变量。我们使用 function (x) ... end 来定义一个函数和使用{}创建一个表一样。
table 标准库提供一个排序函数,接受一个表作为输入参数并且排序表中的元素。这个函数必须能够对不同类型的值(字符串或者数值)按升序或者降序进行排序。Lua 不是尽可能多地提供参数来满足这些情况的需要,而是接受一个排序函数作为参数(类似C++的函数对象),排序函数接受两个排序元素作为输入参数,并且返回两者的大小关系,例如:

network = { 
 {name = "grauna", IP = "210.26.30.34"}, 
 {name = "arraial", IP = "210.26.30.23"}, 
 {name = "lua", IP = "210.26.23.12"}, 
 {name = "derain", IP = "210.26.23.20"}, 
} 

如果我们想通过表的 name 域排序:

table.sort(network, function (a,b) 
        return (a.name > b.name) 
end) 

以其他函数作为参数的函数在 Lua 中被称作高级函数(higher-order function),如上面的 sort。在 Lua 中,高级函数与普通函数没有区别,它们只是把“作为参数的函数”当作第一类值(first-class value)处理而已。
下面给出一个绘图函数的例子:

function eraseTerminal() 
 io.write("\27[2J") 
end 
-- writes an '*' at column 'x' , 'row y' 
function mark (x,y) 
 io.write(string.format("\27[%d;%dH*", y, x)) 
end 
-- Terminal size 
TermSize = {w = 80, h = 24} 
-- plot a function 
-- (assume that domain and image are in the range [-1,1]) 
function plot (f) 
 eraseTerminal() 
for i=1,TermSize.w do
 local x = (i/TermSize.w)*2 - 1 
 local y = (f(x) + 1)/2 * TermSize.h 
 mark(i, y) 
end 
 io.read() -- wait before spoiling the screen 
end 

要想让这个例子正确的运行,你必须调整你的终端类型和代码中的控制符一致:
plot(function (x) return math.sin(x*2*math.pi) end)
将在屏幕上输出一个正弦曲线。
将第一类值函数应用在表中是 Lua 实现面向对象和包机制的关键

  • 闭包
    当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。
    下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表;现在想根据学生的成绩从高到低对学生进行排序,可以这样做:
names = {"Peter", "Paul", "Mary"} 
grades = {Mary = 10, Paul = 7, Peter = 8} 
table.sort(names, function (n1, n2) 
return grades[n1] > grades[n2] -- compare the grades 
end) 

假定创建一个函数实现此功能:

function sortbygrade (names, grades) 
 table.sort(names, function (n1, n2) 
 return grades[n1] > grades[n2] -- compare the grades 
end) 
end 

例子中包含在 sortbygrade 函数内部的 sort 中的匿名函数可以访问 sortbygrade 的参数grades,在匿名函数内部 grades 不是全局变量也不是局部变量,我们称作外部的局部变量(external local variable)或者 upvalue。(upvalue 意思有些误导,然而在 Lua 中他的存在有历史的根源,还有他比起 external local variable 简短)。看下面的代码:

function newCounter() 
local i = 0 
return function() -- anonymous function 
 i = i + 1 
 return i 
end 
end 
c1 = newCounter() 
print(c1()) --> 1 
print(c1()) --> 2 

匿名函数使用 upvalue i 保存他的计数,当我们调用匿名函数的时候 i 已经超出了作用范围,因为创建 i 的函数 newCounter 已经返回了。然而 Lua 用闭包的思想正确处理了这种情况。简单的说,闭包是一个函数以及它的 upvalues。如果我们再次调用 newCounter,将创建一个新的局部变量 i,因此我们得到了一个作用在新的变量 i 上的新闭包。

c2 = newCounter() 
print(c2()) --> 1 
print(c1()) --> 3 
print(c2()) --> 2 

c1、c2 是建立在同一个函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包
技术上来讲,闭包指值而不是指函数,函数仅仅是闭包的一个原型声明;尽管如此,在不会导致混淆的情况下我们继续使用术语函数代指闭包。闭包在上下文环境中提供很有用的功能,如前面我们见到的可以作为高级函数(sort)的参数;作为函数嵌套的函数(newCounter)。这一机制使得我们可以在 Lua 的函数世界里组合出奇幻的编程技术。闭包也可用在回调函数中,比如在 GUI 环境中你需要创建一系列button,但用户按下 button 时回调函数被调用,可能不同的按钮被按下时需要处理的任务有点区别。具体来讲,一个十进制计算器需要 10 个相似的按钮,每个按钮对应一个数字,可以使用下面的函数创建他们:

function digitButton (digit) 
return Button{ label = digit, 
 action = function () 
 add_to_display(digit) 
 end 
 } 
end 

这个例子中我们假定 Button 是一个用来创建新按钮的工具, label 是按钮的标签,action 是按钮被按下时调用的回调函数。(实际上是一个闭包,因为他访问 upvalue digit)。digitButton 完成任务返回后,局部变量 digit 超出范围,回调函数仍然可以被调用并且可以访问局部变量 digit。

闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或者预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。例如你可以重定义 sin 使其接受一个度数而不是弧度作为参数:

oldSin = math.sin 
math.sin = function (x) 
return oldSin(x*math.pi/180) 
end 

更清楚的方式:

do 
local oldSin = math.sin 
local k = math.pi/180 
 math.sin = function (x) 
 return oldSin(x*k) 
end 
end 

这样我们把原始版本放在一个局部变量内,访问 sin 的唯一方式是通过新版本的函数。

利用同样的特征我们可以创建一个安全的环境(也称作沙箱,和 java 里的沙箱一样),
当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境
是需要的,比如我们可以使用闭包重定义 io 库的 open 函数来限制程序打开的文件。

do 
    local oldOpen = io.open 
    io.open = function (filename, mode) 
        if access_OK(filename, mode) then
            return oldOpen(filename, mode) 
        else 
            return nil, "access denied"
        end 
    end 
end
  • 非全局函数
    Lua 中函数可以作为全局变量也可以作为局部变量,我们已经看到一些例子:函数作为 table 的域(大部分 Lua 标准库使用这种机制来实现的比如 io.read、math.sin)。这种情况下,必须注意函数和表语法:
  1. 表和函数放在一起
Lib = {} 
Lib.foo = function (x,y) return x + y end
Lib.goo = function (x,y) return x - y end

2.使用表构造函数

Lib = {
    foo = function(x,y)
        return x+y
    end,
    goo = function(x,y)
        return x-y
    end
}

3.Lua提供另一种语法方式

Lib = {}
function Lib.foo(x,y)
    return x+y
end
function Lib.goo(x,y) 
    return x-y
end

当我们将函数保存在一个局部变量内时,我们得到一个局部函数,也就是说局部函数像局部变量一样在一定范围内有效。这种定义在包中是非常有用的:因为 Lua 把 chunk当作函数处理,在 chunk 内可以声明局部函数(仅仅在 chunk 内可见),词法定界保证了包内的其他函数可以调用此函数。下面是声明局部函数的两种方式:

  1. 方式一
local f = function (...) 
 ... 
end 
local g = function (...) 
 ... 
 f() -- external local `f' is visible here 
 ... 
end 
  1. 方式二
local function f (...) 
 ... 
end 

有一点需要我们注意的是在声明递归局部函数的方式:

local fact = function (n )

        if n == 0 then
            return 1
        else 
            return n*fact(n-1)  -- buggy
        end
end

上面这种方式导致 Lua 编译时遇到 fact(n-1)并不知道他是局部函数 fact,Lua 会去查
找是否有这样的全局函数 fact。为了解决这个问题我们必须在定义函数以前先声明:

local fact

fact = function (n )

        if n == 0 then
            return 1
        else 
            return n*fact(n-1)  -- buggy
        end
end

这样在 fact 内部 fact(n-1)调用是一个局部函数调用,运行时 fact 就可以获取正确的值了。
但是 Lua 扩展了他的语法使得可以在直接递归函数定义时使用两种方式都可以。
在定义非直接递归局部函数时要先声明然后定义才可以:

local f, g -- `forward' declarations 
function g () 
 ... f() ... 
end 
function f () 
 ... g() ... 
end 
  • 正确的尾调用
    Lua中函数的另一个有趣的特征是可以正确处理尾调用(尾递归)
    尾调用是一种类似在函数结尾的 goto 调用,当函数最后一个动作是调用另一个函数时,我们称这调用尾调用。例如:
function f(x)
    return g(x)
end

g的调用是尾调用。
例子中f调用g后不会再做任何事,这种情况当被调用函数g结束时程序不需要返回到调用者f;所以尾调用之后程序不需要再栈中保留关于调用者任何信息。一些编译器比如Lua解释器利用这特性再处理调用时不适用额外的栈,称这种语言支持正确的尾调用。
由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。例如下面调用不论 n 为何值不会导致栈溢出。

function foo (n) 
if n > 0 then return foo(n - 1) end
end 

需要注意的是:必须明确什么是尾调用。
一些调用者函数调用其他函数后也没有做其他的事情但不属于尾调用。比如:

function f (x) 
    g(x) 
    return 
end 

上面这个例子中 f 在调用 g 后,不得不丢弃 g 地返回值,所以不是尾调用,同样的下面几个例子也不时尾调用:

return g(x) + 1 -- must do the addition 
return x or g(x) -- must adjust to 1 result 
return (g(x)) -- must adjust to 1 result 

Lua 中类似 return g(...)这种格式的调用是尾调用。但是 g 和 g 的参数都可以是复杂
表达式,因为 Lua 会在调用之前计算表达式的值。例如下面的调用是尾调用:

return x[i].foo(x[j] + a*b, i + j) 

可以将尾调用理解成一种 goto,在状态机的编程领域尾调用是非常有用的。状态机
的应用要求函数记住每一个状态,改变状态只需要 goto(or call)一个特定的函数。我们考
虑一个迷宫游戏作为例子:迷宫有很多个房间,每个房间有东西南北四个门,每一步输
入一个移动的方向,如果该方向存在即到达该方向对应的房间,否则程序打印警告信息。
目标是:从开始的房间到达目的房间。
这个迷宫游戏是典型的状态机,每个当前的房间是一个状态。我们可以对每个房间
写一个函数实现这个迷宫游戏,我们使用尾调用从一个房间移动到另外一个房间。一个
四个房间的迷宫代码如下:

function room1 () 
local move = io.read() 
if move == "south" then
 return room3() 
elseif move == "east" then
return room2() 
else 
 print("invalid move") 
 return room1() -- stay in the same room 
end 
end 
function room2 () 
local move = io.read() 
if move == "south" then
 return room4() 
elseif move == "west" then
 return room1() 
else 
 print("invalid move") 
 return room2() 
end 
end 
function room3 () 
local move = io.read() 
if move == "north" then
 return room1() 
elseif move == "east" then
 return room4() 
else 
 print("invalid move") 
 return room3() 
end 
end 
function room4 () 
 print("congratilations!") 
end 

我们可以调用 room1()开始这个游戏。
如果没有正确的尾调用,每次移动都要创建一个栈,多次移动后可能导致栈溢出。
但正确的尾调用可以无限制的尾调用,因为每次尾调用只是一个 goto 到另外一个函数并
不是传统的函数调用。

推荐阅读更多精彩内容