Step By Step(Lua函数)
Step By Step(Lua函数)
一、函数:
在Lua中函数的调用方式和C语言基本相同,如:print("Hello World")和a = add(x, y)。唯一的差别是,如果函数只有一个参数,并且该参数的类型为字符串常量或table的构造器,那么圆括号可以省略,如print "Hello World"和f {x = 20, y = 20}。
Lua为面对对象式的调用也提供了一种特殊的语法--冒号操作符。表达式o.foo(o,x)的另一种写法是o:foo(x)。冒号操作符使调用o.foo时将o隐含的作为函数的第一个参数。
Lua中函数的声明方式如下:
function add(a)
local sum = 0
for i, v in ipairs(a) do
sum = sum + v
end
return sum
end
在以上声明中,包含了函数名(add),参数列表(a),以及函数体。需要说明的是,Lua中实参和形参的数量可以不一致,一旦出现这种情况,Lua的处理规则等同于多重赋值,即实参多于形参,多出的部分被忽略,如果相反,没有被初始化的形参的缺省值为nil。
1. 多重返回值:
Lua支持返回多个结果值。如:
1 s,e = string.find("Hello Lua users","Lua")
2 print("The begin index is " .. s .. ", the end index is " .. e .. ".");
3 -- The begin index is 7, the end index is 9.
以上的代码示例只是演示了如何获取Lua函数的多个返回值,下面的示例将给出如何声明返回多个值的Lua函数。如:
1 function maximum(a)
2 local mi = 1
3 local m = a[mi]
4 for i, val in ipairs(a) do
5 if val > m then
6 mi,m = i,val
7 end
8 end
9 return m,mi
10 end
11 print(maximum{8,10,23,12,5})
12 --23 3
Lua会调整一个函数的返回值数量以适应不同的调用情况。若将函数调用作为一条单独语句时,Lua会丢弃函数的所有返回值。若将函数作为表达式的一部分来调用时,Lua只保留函数的第一个返回值。只有当一个函数调用是一系列表达式中的最后一个元素时,才能获得所有返回值。这里先给出三个样例函数,如:
function foo0() end
function foo1() return "a" end
function foo2() return "a","b" end
| 示例代码 | 结果 | 注释 |
| x,y = foo2() | x = "a", y = "b" | 函数调用时最后的(或仅有的)一个表达式,Lua会保留其尽可能多的返回值,用于匹配赋值变量。 |
| x = foo2() | x = "a", 返回值"b"被忽略 | |
| x,y,z = 10,foo2() | x = 10, y = "a", z = "b" | |
| x,y = foo0() | x = nil, y = nil | 如果一个函数没有返回值或者没有足够多的返回值,那么Lua会用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被忽略。 | |
| print(foo0()) | 当一个函数调用左右另一个函数调用的最后一个实参时,第一个函数的所有返回值都将作为实参传入第二个函数。 | |
| print(foo1()) | a | |
| print(foo2()) | a b | |
| print(foo2(),1) | a 1 | |
| t = {foo0()} | t = {} --空table | table构造器可以完整的接收一个函数调用的所有结果,即不会有任何数量方面的调整。 |
| t = {foo1()} | t = {"a"} | |
| t = {foo2()} | t = {"a", "b"} | |
| t = { foo0(), foo2(), 4} | t[1] = nil, t[2] = "a", t[3] = 4 | 如果函数调用不是作为最后一个元素,那么只返回函数的第一个结果值。 |
| print((foo2())) | a | 如果函数调用放入圆括号中,那么Lua将只返回该函数的第一个结果值。 |
最后一个需要介绍的是Lua中unpack函数,该函数将接收数组作为参数,并从下标1开始返回该数组的所有元素。如:
/> lua
> print(unpack{10,20,30})
10 20 30
> a,b = unpack{10,20,30}
> print(a,b)
10 20
> string.find(unpack{"hello","ll"}) --等同于string.find("hello","ll")
在Lua中unpack函数是用C语言实现的。为了便于理解,下面给出在Lua中通过递归实现一样的效果,如:
1 function unpack(t,i)
2 i = i or 1
3 if t[i] then
4 return t[i], unpack(t,i + 1)
5 end
6 end
2. 变长参数:
Lua中的函数可以接受不同数量的实参,其声明和使用方式如下:
1 function add(...)
2 local s = 0
3 for i, v in ipairs{...} do
4 s = s + v
5 end
6 return s
7 end
8 print(add(3,4,5,6,7))
9 --输出结果为:25
解释一下,函数声明中的(...)表示该函数可以接受不同数量的参数。当这个函数被调用时,所有的参数都被汇聚在一起,函数中访问它时,仍需用3个点(...)。但不同的是,此时这3个点将作为表达式来使用,如{...}表示一个由所有变参构成的数组。在含有变长参数的函数中个,同样可以带有固定参数,但是固定参数一定要在变长参数之前声明,如:
function test(arg1,arg2,...)
...
end
关于Lua的变长参数最后需要说明的是,由于变长参数中可能包含nil值,因此再使用类似获取table元素数量(#)的方式获取变参的数量就会出现问题。如果要想始终获得正确的参数数量,可以使用Lua提供的select函数,如:
1 for i = 1, select('#',...) do --这里'#'值表示让select返回变参的数量(其中包括nil)。
2 local arg = select(i, ...) --这里的i表示获取第i个变参,1为第一个。
3 --do something
4 end
3. 具名实参:
在函数调用时,Lua的传参规则和C语言相同,并不真正支持具名实参。但是我们可以通过table来模拟,比如:
function rename(old,new)
...
end
这里我们可以让上面的rename函数只接收一个参数,即table类型的参数,与此同时,该table对象将含有old和new两个key。如:
function rename(arg)
local old = arg.old
local new = arg.new
...
end
这种修改方式有些类似于JavaBean,即将多个参数合并为一个JavaBean。然而在使用时,Lua的table存在一个天然的优势,即如果函数只有一个参数且为string或table类型,在调用该函数时,可以不用加圆括号,如:
rename {old = "oldfile.txt", new = "newfile.txt"}
二、深入函数:
在Lua中函数和所有其它值一样都是匿名的,即它们都没有名称。在使用时都是操作持有该函数的变量,如:
a = { p = print }
a.p("Hello World")
b = print
b("Hello World")
在声明Lua函数时,可以直接给出所谓的函数名,如:
function foo(x) return 2 * x end
我们同样可以使用下面这种更为简化的方式声明Lua中的函数,如:
foo = function(x) return 2 * x end
因此,我们可以将函数理解为由语句构成的类型值,同时将这个值赋值给一个变量。由此我们可以将表达式"function(x) <body> end"视为一种函数的构造式,就想table的{}一样。我们将这种函数构造式的结果称为一个"匿名函数"。下面的示例显示了匿名函数的方便性,它的使用方式有些类似于Java中的匿名类,如:
table.sort(test_table,function(a,b) return (a.name > b.name) end)
1. closure(闭合函数):
若将一个函数写在另一个函数之内,那么这个位于内部的函数便可以访问外部函数中的局部变量,见如下示例:
1 function newCounter()
2 local i = 0
3 return function() --匿名函数
4 i = i + 1
5 return i
6 end
7 end
8 c1 = newCounter()
9 print("The return value of first call is " .. c1())
10 print("The return value of second call is " .. c1())
11 --输出结果为:
12 --The return value of first call is 1
13 --The return value of second call is 2
在上面的示例中,我们将newCounter()函数称为闭包函数。其函数体内的局部变量i被称为"非局部变量",和普通局部变量不同的是该变量被newCounter函数体内的匿名函数访问并操作。再有就是在函数newCounter返回后,其值仍然被保留并可用于下一次计算。再看一下下面的调用方式。
1 function newCounter()
2 local i = 0
3 return function() --匿名函数
4 i = i + 1
5 return i
6 end
7 end
8 c1 = newCounter()
9 c2 = newCounter()
10 print("The return value of first call with c1 is " .. c1())
11 print("The return value of first call with c2 is " .. c2())
12 print("The return value of second call with c1 is " .. c1())
13 --输出结果为:
14 --The return value of first call with c1 is 1
15 --The return value of first call with c2 is 1
16 --The return value of second call with c1 is 2
由此可以推出,Lua每次在给新的闭包变量赋值时,都会让不同的闭包变量拥有独立的"非局部变量"。下面的示例将给出基于闭包的更为通用性的用法:
1 do
2 --这里将原有的文件打开函数赋值给"私有变量"oldOpen,该变量在块外无法访问。
3 local oldOpen = io.open
4 --新增一个匿名函数,用于判断本次文件打开操作的合法性。
5 local access_OK = function(filename,mode) <检查访问权限> end
6 --将原有的io.open函数变量指向新的函数,同时在新函数中调用老函数以完成真正的打开操作。
7 io.open = function(filename,mode)
8 if access_OK(filename,mode) then
9 return oldOpen(filename,mode)
10 else
11 return nil,"Access denied"
12 end
13 end
14 end
上面的这个例子有些类似于设计模式中装饰者模式。
2. 非全局函数:
从上一小节中可以看出,Lua中的函数不仅可以直接赋值给全局变量,同时也可以赋值给其他类型的变量,如局部变量和table中的字段等。事实上,Lua库中大多数table都带有函数,如io.read、math.sin等。这种写法有些类似于C++中的结构体。如:
Lib = {}
Lib.add = function(x,y) return x + y end
Lib.sub = function(x,y) return x - y end
或者是在table的构造式中直接初始化,如:
Lib = { add = function(x,y) return x + y end,
sub = function(x,y) return x - y end
}
除此之外,Lua还提供另外一种语法来定义此类函数,如:
Lib = {}
function Lib.add(x,y) return x + y end
function Lib.sub(x,y) return x - y end
对于Lua中的局部函数,其语义在理解上也是非常简单的。由于Lua中都是以程序块作为执行单元,因此程序块内的局部函数在程序块外是无法访问的,如:
1 do
2 local f = function(x,y) return x + y end
3 --do something with f.
4 f(4,5)
5 end
对于这种局部函数,Lua还提供另外一种更为简洁的定义方式,如:
local function f(x,y) return x + y end
该写法等价于:
local f
f = function(x,y) return x + y end
3. 正确的尾调用:
在Lua中支持这样一种函数调用的优化,即“尾调用消除”。我们可以将这种函数调用方式视为goto语句,如:
function f(x) return g(x) end
由于g(x)函数是f(x)函数的最后一条语句,在函数g返回之后,f()函数将没有任何指令需要被执行,因此在函数g()返回时,可以直接返回到f()函数的调用点。由此可见,Lua解释器一旦发现g()函数是f()函数的尾调用,那么在调用g()时将不会产生因函数调用而引起的栈开销。这里需要强调的是,尾调用函数一定是其调用函数的最后一条语句,否则Lua不会进行优化。然而事实上,我们在很多看似是尾调用的场景中,实际上并不是真正的尾调用,如:
function f(x) g(x) end --没有return语句的明确提示
function f(x) return g(x) + 1 --在g()函数返回之后仍需执行一次加一的指令。
function f(x) return x or g(x) --如果g()函数返回多个值,该操作会强制要求g()函数只返回一个值。
function f(x) return (g(x)) --原因同上。
在Lua中,只有"return <func>(<args>)"形式才是标准的尾调用,至于参数中(args)是否包含表达式,由于表达式的执行是在函数调用之前完成的,因此不会影响该函数成为尾调用函数。
Step By Step(Lua函数)的更多相关文章
- Step By Step(编写C函数的技巧)
Step By Step(编写C函数的技巧) 1. 数组操作: 在Lua中,"数组"只是table的一个别名,是指以一种特殊的方法来使用table.出于性能原因,Lua的C ...
- Step By Step(C调用Lua)
Step By Step(C调用Lua) 1. 基础: Lua的一项重要用途就是作为一种配置语言.现在从一个简单的示例开始吧. --这里是用Lua代码定义的窗口大小的配置信息 wid ...
- Step By Step(Lua系统库)
Step By Step(Lua系统库) Lua为了保证高度的可移植性,因此,它的标准库仅仅提供了非常少的功能,特别是和OS相关的库.但是Lua还提供了一些扩展库,比如Posix库等.对于文件操作而言 ...
- Step By Step(Lua输入输出库)
Step By Step(Lua输入输出库) I/O库为文件操作提供了两种不同的模型,简单模型和完整模型.简单模型假设一个当前输入文件和一个当前输出文件,他的I/O操作均作用于这些文件.完整模型则使用 ...
- Step By Step(Lua字符串库)
Step By Step(Lua字符串库) 1. 基础字符串函数: 字符串库中有一些函数非常简单,如: 1). string.len(s) 返回字符串s的长度: 2). string ...
- Step By Step(Lua弱引用table)
Step By Step(Lua弱引用table) Lua采用了基于垃圾收集的内存管理机制,因此对于程序员来说,在很多时候内存问题都将不再困扰他们.然而任何垃圾收集器都不是万能的,在有些特殊情况下,垃 ...
- Step By Step(Lua面向对象)
Step By Step(Lua面向对象) Lua中的table就是一种对象,但是如果直接使用仍然会存在大量的问题,见如下代码: 1 Account = {balance = 0}2 function ...
- Step By Step(Lua模块与包)
Step By Step(Lua模块与包) 从Lua 5.1开始,我们可以使用require和module函数来获取和创建Lua中的模块.从使用者的角度来看,一个模块就是一个程序库,可以通过requi ...
- Step By Step(Lua环境)
Step By Step(Lua环境) Lua将其所有的全局变量保存在一个常规的table中,这个table被称为"环境".它被保存在全局变量_G中. 1. 全局变量声明: ...
随机推荐
- 1087 All Roads Lead to Rome
Indeed there are many different tourist routes from our city to Rome. You are supposed to find your ...
- 一文读懂eBPF/XDP
XDP概述 XDP是Linux网络路径上内核集成的数据包处理器,具有安全.可编程.高性能的特点.当网卡驱动程序收到数据包时,该处理器执行BPF程序.XDP可以在数据包进入协议栈之前就进行处理,因此具有 ...
- 基于C++简单Windows API的socket编程(阻塞模式)
1. 概述:简单的基于Windows API的socket点对点聊天程序,为了方便初学者,本文代码均采用阻塞原理编写. 2. 代码样例 Server.cpp(服务端) #include <cst ...
- 三、多线程之Thread与Runnable的区别
Thread与Runnable的区别(用三个窗口同时出售10张车票为例子) 运行代码 运行结果 分析 System.out.println("开始测试多线程");class MyT ...
- MySQL|一文解决主库已有数据的主从复制
主从复制配置方案和实际的场景有很多,在之前配置了主从库都是全新的配置方案 在这一篇会配置主库存在数据,然后配置主从复制 开始之前,先分享一套MySQL教程,小白入门或者学习巩固都可以看 MySQL基础 ...
- 微信小程序中的常见弹框
显示加载中的提示框 wx.showLoading() 当我们正在在进行网络请求时,常常就需要这个提示框 手动调用wx.hideLoading()方法才能够关闭这个提示框,通常在数据请求完毕时就应该关闭 ...
- 关于MySQL参数,这些你要知道
前言: 在前面一些文章中,经常能看到介绍某某参数的作用,可能有些小伙伴仍搞不清楚 MySQL 参数是啥.本篇文章我们来聊聊 MySQL 参数,学习下如何管理维护 MySQL 参数. 1.MySQL参数 ...
- OOP第三章博客
OO第三单元博客 • (1)梳理JML语言的理论基础.应用工具链情况: 理论基础: 网络资料上面介绍JML有两种主要的用法: 开展规格化设计.这样交给代码实现人员的将不是可能带有内在模糊性.二义性的自 ...
- python3读取文件指定行的三种方案
技术背景 考虑到深度学习领域中的数据规模一般都比较大,尤其是训练集,这个限制条件对应到实际编程中就意味着,我们很有可能无法将整个数据文件的内容全部都加载到内存中.那么就需要一些特殊的处理方式,比如:创 ...
- [DB] Spark Streaming
概述 流式计算框架,类似Storm 严格来说不是真正的流式计算(实时计算),而是把连续的数据当做不连续的RDD处理,本质是离散计算 Flink:和 Spark Streaming 相反,把离散数据当成 ...