Lua – 函数

引言

在Lua中,函数是对语句和表达式进行抽象的主要方法。既可以用来处理一些特殊的工作,也可以用来计算一些值。Lua 提供了许多的内建函数,你可以很方便的在程序中调用它们,如print()函数可以将传入的参数打印在控制台上。

Lua 函数主要有两种用途:

1.完成指定的任务,这种情况下函数作为调用语句使用;

2.计算并返回值,这种情况下函数作为赋值语句的表达式使用。

本篇博客,主要讲解了lua中的基本函数,闭包,尾调用,迭代器等知识点

Lua函数基本特性

函数定义

Lua 编程语言函数定义格式如下

parame 解释
optional_function_scope 该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。
function_name 指定函数名称。
argument1, argument2, argument3..., argumentn 函数参数,多个参数以逗号隔开,函数也可以不带参数。
function_body 函数体,函数中需要执行的代码语句块。
result_params_comma_separated 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开。当然也可以没有,函数主要用来完成指定的任务

下面看一个简单的例子

此外你可以把函数当做变量传递,或者保存在一个变量之中,就像C的函数指针一样

多返回值&可变参数

多返回值与可变参数可以说是Lua的一个与众不同的特性

对与多返回值以参数列表形式列举出来就ok,对与可变参数通过 ... 传入,Lua根据实际情况进行参数的匹配,也遵循多弃少补的原则

代码实例

变长参数(...)搭配unpack(lua5.3已经把这个函数移动到了table表中,即使用table.unpack调用)这个库函数使用能适用于传入多个形参同时传出多个形参做一些其他操作的特殊情况

具名实参

一般的语言中,参数传递是具有位置性的,有就是说在参数传递后通过在参数列表的位置中与形参匹配起来,第一个值以第一个形参匹配,第二个值与第二个形参匹配以此类推,但有时候难免记不起函数的参数相对位置,那么通过名字来匹配是一个有效的策略。

lua通过构造一个table表,将参数封装在table中当做参数传递,就可以通过名字到table表中读取

一个简单的例子

闭包函数

在了解闭包之前我们先来了解一下什么是匿名函数

匿名函数

所谓匿名,字面意思理解就是没有指定名称的函数,而事实上也的确如此

传统的一个函数定义如下 function foo (x) return 2*x end 但是这其实就是 foo = function(x) return 2*x end 的一种简写,即一种所谓的“语法糖”,因此一个函数定义实质就是一条赋值语句,这条语句创建了一种类型为“函数”的值,并赋值给一个变量。可以将表达式 function (x) <body> end视为一种函数构造式,就像table的构造式{}一样。这里的 function(x) return 2*x end 就可以称之为一个匿名函数

闭合函数

在Lua中,若将一个函数写在另一个函数之内,那么这个位于内部的函数便可以访问外部函数中的局部变量,这项特征称之为“词法域”。

而闭包(closure)是由一个函数和该函数会访问到的非局部变量(或者是upvalue)组成的,其中非局部变量(non-local variable)是指不是在局部作用范围内定义的一个变量,但同时又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会访问的非局部变量,那么它就是通常说的函数。也就是说,在Lua中,函数是闭包一种特殊情况。

简单总结如下

闭包:通过调用含有一个内部函数加上该外部函数持有的外部局部变量(upvalue)的外部函数(就是工厂)产生的一个实例函数

闭包组成:外部函数+外部函数创建的upvalue+内部函数(闭包函数)

举一个例子说明一下

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

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

在lua中,假设有外部程序在lua的环境中运行,会调用打开文件的api,使用closure来重定义这些api就能实现沙盒的功能。

非全局函数

通常我们在lua中声明和定义一个函数时,不会加上local变量,此时函数是全局函数。在内存中可以被其他文件调用。 举个例子,假设现在有两个lua文件1.lua, 2.lua, 在1.lua中有两个全局函数a, b以及一个局部函数c,在2.lua中怼1.lua的函数进行调用,函数a, b将能成功访问,而c函数将由于访问权限问题无法访问

具体代码如下

在比如说,递归是我们经常用到的一个编程思想,假如我有如下函数

看上去好像并没有什么错误,但是事实上调用发现出错了,这是由于Lua在编译fact时编译到内层fact(n-1)发现找不到fact的定义造成的,这一句语句lua实际上调用了一个全局的fact,而非这个函数自身,要解决这个问题,把函数写成全局就ok(只是可行,不建议),或者像c 或者 c++ 一下事先申明一下就好了,即

尾调用

尾调用定义

尾调用也可以称之为“尾递归”,即递归的最后一个动作是对一个函数的引用,由于当前的递归函数最后一个动作是对一个函数的引用,因此当前的递归函数的上下文对于递归结果已经不重要,在进入对下一个函数的引用时,会把保存在堆栈中的当前递归函数的上下文环境清除,把空间让给下一层递归或函数。

lua中的尾调用

lua语言同样支持尾调用,实现尾调用时,如果希望保存当前局部变量值,需在当前递归函数的最后一个动作将当前需要保存的变量或环境打包为参数传递给下一层递归或函数,并且由于尾调用不会耗费栈空间,所以一个程序可以拥有无数嵌套的“尾调用”

举个例子

当然对于 “尾调用” 很多人都存在一个思维误区,下面的几种情况(假设都是函数f的返回)都不是尾调用

return g(x) + 1 -- 必须做一次加法

return x or g(x) -- 必须调整为一个返回值

return (g(x)) -- 必须调整为一个返回值

原因在于,当调用完g函数后f不能够立即返回,他还要丢弃f返回的临时结果,在Lua中只有对 return <func>(<args>) 这样的调用形式才能算一个尾调用

尾调用的意义在于哪怕他是无限调用的代码,程序运行时内存消耗都能保持一个相对稳定的状态,不存在崩溃的可能(当然代码写的没问题orz)

可以用下列代码的两个函数分别跑着看看,体会尾调用的好处

Hint: 尾调用一般应用在实现游戏状态机或实现广度、深度搜索等方面是非常有用的,得益于尾调用的内存回收,使用了这种类似实现的程序能有更大的空间去完成多种游戏状态的穷举。

迭代器与泛型for

所谓迭代器就是一种能够遍历一种集合中所有元素的机制,每个迭代器对象代表容器中的确定的地址 在Lua中迭代器是一种支持指针类型的结构,通常将迭代器表示为函数,每调用一个函数,即返回集合中的“下一个”元素

迭代器与closure

迭代器一般都涉及到相关状态的保存,所以迭代器和closure的关系就不言而喻了,下面给出一个简单的迭代器例子

当然使用泛型for来调用这个迭代器更简单,事实上就是为它而设计的

泛型for为一次迭代器循环做了所有的薄记动作,在内部保存了迭代器函数,因此就省去了iter这个变量,一般来说迭代器,都具有迭代器本身编写复杂,使用简单的特征

泛型 for 迭代器

泛型 for 在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。

泛型 for 迭代器提供了集合的 key/value 对,语法格式如下:

一个简单例子如下

以上实例中我们使用了 Lua 默认提供的迭代函数 ipairs。

下面我们看看泛型 for 的执行过程:

=== 首先,初始化,计算 in 后面表达式的值,表达式应该返回范性 for 需要的三个值:迭代函数、状态常量(array)、控制变量(遍历的下标);与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。

=== 第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。

=== 第三,将迭代函数返回的值赋给变量列表。

=== 第四,如果返回的第一个值为nil循环结束,否则执行循环体。

=== 第五,回到第二步再次调用迭代函数

在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。Lua 的迭代器包含以下两种类型:

  1. 无状态的迭代器
  2. 多状态的迭代器(复杂状态)

无状态的迭代器

“无状态的迭代器” 正如名字说的一样是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。这种无状态迭代器的典型的简单的例子是ipairs,调用 next(t, nil),他遍历数组的每一个元素。同时还有一个pairs调用 next(t, k),k为table中的键值,返回table中的下一个 key 及 当前 key 对应的值, 这个调用是返回table中任意次序的一组值

以下实例我们使用了一个简单的函数来实现迭代器,实现 数字 n 的平方:

迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs和迭代函数都很简单,我们在Lua中可以这样实现:

当Lua调用ipairs(a)开始循环时,他获取三个值:迭代函数iter、状态常量a、控制变量初始值0;然后Lua调用iter(a,0)返回1,a[1](除非a[1]=nil);第二次迭代调用iter(a,1)返回2,a[2]……直到第一个nil元素。

多状态的迭代器(复杂状态)

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到table内,将table作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在table内,所以迭代函数通常不需要第二个参数。

下面我们通过闭包来实现一个迭代器,遍历一个table表

结语

以上就是Lua中函数相关的一些东西,示例代码直接在用markdown写博客的时候手撸,未经严格测试,如有错误欢迎指正

繁夜

发表评论


:[微笑]::[撇嘴]::[色]::[发呆]::[得意]::[流泪]::[害羞]::[闭嘴]::[睡]::[大哭]::[尴尬]::[发怒]::[调皮]::[呲牙]::[惊讶]::[难过]::[酷]::[冷汗]::[抓狂]::[吐]::[偷笑]::[可爱]::[白眼]::[傲慢]::[饥饿]::[困]::[惊恐]::[流汗]::[憨笑]::[大兵]::[奋斗]::[咒骂]::[疑问]::[嘘...]::[晕]::[折磨]::[衰]::[骷髅]::[敲打]::[再见]::[擦汗]::[抠鼻]::[鼓掌]::[糗大了]::[坏笑]::[左哼哼]::[右哼哼]::[哈欠]::[鄙视]::[委屈]::[快哭了]::[阴险]::[亲亲]::[吓]::[可怜]::[笑哭]::[doge]::[泪奔]::[无奈]::[托腮]::[斜眼笑]::[喷血]::[惊喜]::[骚扰]::[小纠结]::[我最美]::[羊驼]::[幽灵]::[吃]::[OK]::[爱你]::[抱拳]::[勾引]::[强]::[弱]::[拳头]::[爱心]::[喝彩]::[西瓜]::[啤酒]::[玫瑰]::[凋谢]::[礼物]::[拥抱]::[月亮]::[菊花]::[棒棒糖]::[蛋]::[刀]::[菜刀]::[炸弹]::[手枪]:

刷新评论

目前评论:2   其中:访客  2   博主  0

    • 大学生暑假兼职 大学生暑假兼职 0    来自「广东省东莞市 电信」的朋友 火狐浏览器 Windows XP

      小小一个函数,竟带出这么涉及这么广

      • wordpress企业主题 wordpress企业主题 0    来自「河南省郑州市 联通」的朋友 火狐浏览器 Windows 7

        并非技术派,拜访博主,祝好,感谢分享