您的位置:首页 > 编程语言 > Lua

lua(4)-函数

2016-08-11 21:05 134 查看
lua中函数的声明和定义需要使用function关键字,并且用end结束定义区域
(1)Lua的函数没有太严格的形参必须与实参一 一对应的规则,一个函数可以带0个或多个形参,实际调用的时候也可以传递0个或多个实参



输出



(2)Lua里的函数还有一个非常与众不同的规则,就是允许返回多个值



输出



(3)变长参数
使用"..."可以编写出能接受不同数量实参的函数



输出



变长参数(...)搭配unpack这个库函数使用能实现传入多个形参同时传出多个形参在做一些类似于需要传递一个函数指针,又能同时传递多个不限定的参数,又能使得当调用函数指针的方法的时候使用传入的多个参数
举个例子



输出




(4)closure(闭合函数),可用于排序(sort)等
若将一个函数写在另一个函数之内,那么这个位于内部的函数便可以访问外部函数中的局部变量,这项特征称之为“词法域”
假设有一个学生姓名的列表和一个对应于每个姓名的年级列表,需要根据每个学生的年级来对他们的姓名进行排序(由高到低)。可以这么做



输出



如果单独使用一个函数来解决这个排序问题,可以这么写



输出



上例的sortbygrade中,grades是sortbygrade函数的局部变量,但是sort中的匿名函数中的grades既不是全局变量也不是局部变量,将其称为一个“非局部的变量(non-local
variable)”

为了正确理解“非局部的变量”这个概念,可以从一个计数器的函数来理解



输出



在上面这个计数器的代码中,匿名函数访问了一个“非局部变量”i,该变量用于保持一个计数器,初看上去,由于创建变量i的函数(newCounter)已经返回,所以之后每次调用匿名函数时,i都应是已超出了作用范围的。但其实不然,Lua会以closure的概念来正确地处理这种情况,简单的讲,一个closure就是一个函数加上该函数所需访问的所有“非局部的变量”。如果再次调用newCounter,那么它会创建一个新的局部变量i,从而也将得到一个新的closure



输出



我们来对这个计数器扩展一下,让这个计时器可以按照自定义的时间间隔来进行计数



输出



利用closure的“非局部变量”的特性,lua还可以实现一种类似于沙盒功能的程序。沙盒为外部程序的运行提供了一个安全受限的环境,比如常见的在Android系统中,安装的应用程序部分是在沙盒中运行的,这些应用程序调用了Android系统的api(如打开和读写文件),在沙盒中运行这些应用程序时,应用程序调用的系统api往往被重写过,加上了限制条件,如果应用程序通过了条件的限制,就能成功调用,否则将调用失败。
在lua中,假设有外部程序在lua的环境中运行,会调用打开文件的api,使用closure来重定义这些api就能实现沙盒的功能。



上述的示例中,原库函数io.open被保存在一个局部变量oldOpen中,外部程序如果想要调用io.open将会调用下面被重定义的io.open,而这个重定义的io.open会对打开的文件和方式做一个检查,如果检查通过,才能被允许用原来的库函数打开和读写文件。

(5)非全局的函数
通常我们在lua中声明和定义一个函数时,不会加上local变量,此时函数是全局函数。在内存中可以被其他文件调用。
举个例子,假设我电脑上F盘有个abc.lua,abc.lua中有三个函数,其中a、b是全局函数,c是局部函数。



同样在F盘中新建一个abc2.lua文件,abc2.lua文件调用a、b函数。



可以看到输出结果,已经调用成功。



修改一下abc2.lua,尝试调用函数c。





能看到由于访问权限的限制,c函数不能正常被访问。

全局函数的调用不需要前置声明,而局部函数的调用需要注意前置声明。
举个例子,abc.lua中有全局函数a、b,局部函数c,其中a函数里对b、c函数具有调用。



查看输出结果,可以看到a函数对b函数的调用成功了,但是a函数对c函数的调用失败了。



由于lua存在“词法域”,当函数作为局部变量声明和定义时,该函数只在该函数的程序块部分及当前文件以下部分的程序块中是可见状态,因此想要获得对c函数的调用,还需要对c函数做一个前置声明。





可以看到在对c函数做了一个前置声明后,a函数就能正常的访问到c函数了。

(6)局部函数的递归
对于全局函数,lua函数的递归与其他语言的递归并没有太大的不同;对于局部函数的递归,在进入下一轮递归时,如果需要调用自身或处于当前代码行下面的局部函数,则需要前置声明。举个例子,假设有局部函数a,a对自身递归调用。





结果输出报错,原因在(5)中有提到,由于lua的“词法域”的特性,局部函数将只在当前函数声明和定义以及下文处能被调用,a(n-1)+a(n-2)出现在a函数定义未结束之前(也就是end之前),因此会提示找不到函数a。加上前置声明即可恢复调用。





如图所示,局部函数a的递归调用已经能够成功运行。

(7)尾调用
尾调用也可以称之为“尾递归”,即递归的最后一个动作是对一个函数的引用,由于当前的递归函数最后一个动作是对一个函数的引用,因此当前的递归函数的上下文对于递归结果已经不重要,在进入对下一个函数的引用时,会把保存在堆栈中的当前递归函数的上下文环境清除,把空间让给下一层递归或函数。lua语言同样支持尾调用,实现尾调用时,如果希望保存当前局部变量值,需在当前递归函数的最后一个动作将当前需要保存的变量或环境打包为参数传递给下一层递归或函数。
如下图所示,在recursion0函数中不使用尾调用执行无限递归,而recursion1使用尾调用来执行无限递归,首先看看recursion0的执行结果。





可以看到recursion0的无限递归造成了堆栈溢出。
这回我们来运行recursion1。





可以看到recursion1里的无限递归并没有造成堆栈溢出。这一步可以到windows的任务管理器查看当前lua进程的内存使用状态。





而此次运行的lua实例的内存也一直保持在稳定状态,说明recursion1里已经实现了尾递归调用。
另外,我们也可以使用lua的内存管理接口检测当前的递归消耗的内存情况。
在recursion0和recursion1里面添加打印当前lua使用的内存量的语句。







可以看到执行recursion0时从开始到堆栈溢出,递归使用的内存也一直在增长。
再来看看recursion1函数的运行情况。





可以看到,使用了尾调用的recursion1函数在无限递归时使用的内存也一直是保持稳定状态的。
尾调用的应用在实现游戏状态机或实现广度、深度搜索等方面是非常有用的,得益于尾调用的内存回收,使用了这种类似实现的程序能有更大的空间去完成多种游戏状态的穷举。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Lua 脚本语言 函数