Lua 数据类型 —— 函数

一、函数结构

1、声明

optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
    function_body
    return result_params_comma_separated
end
  • optional_function_scope: 该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果需要设置函数为局部函数需要使用关键字 local。
  • function_name: 指定函数名称。
  • argument1, argument2, argument3..., argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。参数的行为与局部变量的行为完全一致(可以通过下面的例子体会)。
  • function_body: 函数体,函数中需要执行的代码语句块。
  • result_params_comma_separated: 函数返回值,Lua 语言函数可以返回多个值,每个值以逗号隔开。

入参的行为

print("参数局部变量:")
function params(a, b)
    print("函数内部,未改变值:", a, b)     --> 函数内部,未改变值:	1	2
    a = 10
    b = 100
    print("函数内部,未改变值:", a, b)     --> 函数内部,未改变值:	10	100
end
local a = 1
local b = 2
print("入参前:", a, b)     --> 入参前:	1	2
params(a, b)
print("入参后:", a, b)     --> 入参后:	1	2

2、使用

函数的调用和 java 、 kotlin 没有区别

只是如果函数只有一个参数,且该参数类型是 字符串常量 或是 表构造器 ,则括号是可选,只需隔开一个空格。

-- 等同于 print("function")
print "function"                 --> function
-- 等同于 dofile("字符串.lua")
dofile "字符串.lua"
-- 等同于 type({})
print(type {})                   --> table

二、入参

1、入参个数

参数个数没有规定一定要和函数声明的个数一致,多余会被抛弃,不足参数会被设置为 nil .

function multiParams(p1, p2, p3, p4, p5)
    print(p1, p2, p3, p4, p5)
end
multiParams()                           --> nil	nil	nil	nil	nil
multiParams(1)                          --> 1	nil	nil	nil	nil
multiParams(1, 2)                       --> 1	2	nil	nil	nil
multiParams(1, "jiang", 2)              --> 1	jiang	2	nil	nil
multiParams(1, "jiang", 2, "peng")      --> 1	jiang	2	peng	nil
multiParams(1, "jiang", 2, "peng", 3)   --> 1	jiang	2	peng	3
multiParams(1, "jiang", 2, "peng", 3, "yong")   --> 1	jiang	2	peng	3

可以利用这一特性,加上 a or 默认值 达到默认值

local globalCounter = 0
function initGlobal(n)
    n = n or 1
    globalCounter = globalCounter + n;
end
initGlobal()
print(globalCounter)            --> 1
initGlobal(10086)
print(globalCounter)            --> 10087

2、使用 ... 接收可变参数

2-1、使用列表进行装载可变长参数

使用 ... 进行接收可变长参数函数,用 {} 装载则将其转为列表,值得注意的是,这里每次调用都会创建一个临时的表

和 kotlin 一样, _ 可以避免无用参数取名

function add(...)
    local total = 0
    for _, v in ipairs { ... } do
        total = total + v;
    end
    return total
end

print(add(3, 4, 10, 25, 12))            --> 54
-- 如果存在 nil , 则后面的就不再继续,因为使用的是 ipairs , 使用 paris 就不会
print(add(3, 4, 10, nil, 25, 12))       --> 17

可以将 ... 当作多值返回(见下一小节)来使用,直接将其赋值给变量

-- ... 和多值返回类似,可以用多值返回的所有操作
-- 多的会被舍弃,少的用 nil 补充
function foo(...)
    local a, b, c = ...
    print(a, b, c, "size: " .. #{ ... })
end

foo("a")                            --> a	nil	nil size: 1
foo("a", "b")                       --> a	b	nil size: 2
foo("a", "b", "c")                  --> a	b	c	size: 3
foo("a", "b", "c", "d")             --> a	b	c	size: 4
foo("a", "b", nil, "c", "d", nil)   --> a	b	nil	size: 5     -- 这里的最后一个元素 `nil` 则会被忽视,如果需要被计算则需要用 `table.pack()`

2-2、使用 select 解析可变长参数

nil 也会被认为是元素

当第一个参数 selector 是数值时,表示从哪个下标开始截取。(开始下标是 1 )

当第一个参数是 # 时,则表示获取长度( nil 也会被包括在内)。

print(select(1, "a", "b", "c"))             --> a	b	c
print(select(1, "a", "b", nil, "c"))        --> a	b	nil	c
print(select(2, "a", "b", "c"))             --> b	c
print(select(3, "a", "b", "c"))             --> c
print(select(4, "a", "b", "c"))             -->       “空”
print(select("#", "a", "b", "c"))           --> 3
-- nil 也会被计算
print(select("#", "a", "b", nil, "c", nil)) --> 5

可以使用 select 进行遍历处理

function add1(...)
    local s = 0
    for i = 1, select("#", ...) do
        -- 此处的 select 只会使用 i 下标的值
        s = s + select(i, ...)
    end
    return s
end
print(add1(1, 2, 3, 4, 5))

2-3、{ ... } 和 select 的区别

两种都可以进行遍历元素。

在元素比较少的情况下, select 会快一些,可以避免每次都创建临时新表

元素较多的情况下,则 { ... } 会快一些

3、... 行为和多值返回行为一样

可以进行多值赋值,多的参数会被抛弃,少的用 nil 补充

对于所有的函数入参都可以使用可变长参数接收,基于这一特性,我们可以使用以下函数进行跟踪一些入参,调试的时候很有用

function showParams(fun, ...)
    print(...)
    return fun(...)
end

-- 使用
print(showParams(add, 1, 2, 3, 4, 5))
--> 1	2	3	4	5
--> 15

4、和固定参数混用

可变参数可以和固定参数一起使用,则像 kotlin 、 java 一样,需要将固定参数放在函数参数的最前面。

三、多值返回

函数可以在 return 中返回多个值

  • 函数只是作为一条单独语句使用,则所有返回值会被舍弃。(即不需要其返回值)
  • 如果函数被当作表达式(加法等操作,字符连接),则只会使用第一个返回值。(因为此处只需要一个值,所以只取第一个返回值)
  • 函数被当作多重赋值、函数调用时传入的实参列表、表构造器、return语句时,多值返回才会被获取。

遵循几个规则:

  1. 赋值时,参数多了会被舍弃,少了会用 nil 进行赋值
  2. 多值返回函数只有作为最后一个表达式时,才会使用每个返回值,否则只使用第一个。无论是在多重赋值,还是在函数入参,表构造器都是一样的。

可以使用增加括号,让多值返回变为只有一个值返回。

举亿个例子

function f0()
    return
end
function f1()
    return "jiang"
end
function f2()
    return "peng yong", 28
end

获取多值返回

-- 多余的返回值会被舍弃
value = f2()
print(value)                        --> a

-- 按顺序进行赋值,多余的返回值会被舍弃,少的则会用 nil
value1, value2, value3 = 10, f2()
print(value1, value2, value3)       --> 10	a	1
value4, value5, value6, value7 = 10, f2()
print(value4, value5, value6, value7)   --> 10	a	1	nil

-- 如果多值返回不是最后一个表达式,则只会使用一个,这个原理同样适用于函数调用入参
value8, value9, value10 = f2(), 10
print(value8, value9, value10)      --> a	10	nil

---- f0 返回 nil 赋值给 value11 , 而 10001 多了一个参数则被舍弃 
value11, value12 = f0(), 10000, 10001
print(value11, value12)             --> nil	10000

将多值返回当作入参

function params(param1, param2, param3)
    print("params:" .. param1 .. "," .. param2 .. ",", param3)
end
---- 唯一一个入参,则会将所有的返回值作为入参
params(f2())                    --> params:peng yong,28,	nil

-- f2() 虽然返回两个参数,但是因为不是 最后一个参数(也不是唯一一个参数) 所以只会使用第一个返回值,则这里只是入参了两个参数
params(f2(), 10000)             --> params:peng yong,10000,	nil

---- f2() 最后一个参数,所以会进行多返回值展开,将所有的 f2() 返回值当作入参,所以这里就有三个入参值
params(10001, f2())             --> params:10001,peng yong,	28

-- 多值返回和其他的运算操作只会使用第一个返回值
print(f2() .. 1)                --> peng yong1

-- 可以作为表的构造器,但也遵循同样的规则,只有作为最后一个表达式,才会使用所有返回值
t1 = { f2() }
t2 = { f2(), "jiang pengyong" }
showTable(t1)                   --> [1]=peng yong, [2]=28, 
showTable(t2)                   --> [1]=peng yong, [2]=jiang pengyong, 

使用括号屏蔽多值返回,则只会使用第一个返回值

-- 使用括号,强制只返回一个值,f2() 则只有第一个返回值被使用,其他的被省略
print((f2()))                   --> peng yong

五、table.pack 和 table.unpack

1、table.pack

如果 {...} 中包含有 nil ,则不再是一个有效的序列。 table.pack (5.2 引入) 会将所有的参数(包括 nil),保存到一个 table 中并返回,而且会有一个额外的字段 “n” 用于保存参数个数 。

function noNils(...)
    local arg = table.pack(...)
    local argValue = ""
    for i = 1, arg.n do
        argValue = argValue .. (arg[i] or "nil") .. " ,";
    end
    print("arg [" .. argValue .. "] size:" .. arg.n)
    for i = 1, arg.n do
        if arg[i] == nil then
            return false
        end
    end
    return true
end

-- 使用
print(noNils(2, 3, nil))        -->  arg [2 ,3 ,nil ,] size:3           false
print(noNils(2, 3))             --> arg [2 ,3 ,] size:2                 true
print(noNils(2, nil, 3))        --> arg [2 ,nil ,3 ,] size:3            false
print(noNils())                 --> arg [] size:0                       true
print(noNils(nil, nil))         --> arg [nil ,nil ,] size:2             false

2、table.unpack

作用是将 table 转换为一组返回值,可以作为函数的入参

unpack 函数的重要用途之一体现在泛型调用,泛型调用允许我们动态地调用具有任意参数的任意函数。

两种使用方式

table.unpack(table) 将 table 进行展开,作为多值返回

table1 = { "jiang", "peng", "yong", "xiao" }
print(table.unpack(table1))         --> jiang	peng	yong	xiao

table.unpack(table, startIndex, endIndex) 截取 table 的 startIndex 至 endIndex 区间的 item ,进行返回 ( [startIndex, endIndex] )

table1 = { "jiang", "peng", "yong", "xiao" }
print(table.unpack(table1, 2, 3))   --> peng	yong

值得注意的是 table.unpack 使用长度操作符获取返回值的个数,因而该函数只能用于序列。

动态调用

这样其实就可以动态的进行调用代码,将上面的稍作修改,可以通过将 print 赋予给变量在进行调用

f = print
f(table.unpack(table1))  --> jiang	peng	yong	xiao

用 Lua 实现一个简易的 table.unpack 函数

真正的 table.unpack 是由 c 进行编写,也可以用递归进行编写一个类似的

function unpack(t, i, n)
    i = i or 1
    n = n or #t
    if i <= n then
        return t[i], unpack(t, i + 1, n)
    end
end

print(unpack(table1))           --> jiang	peng	yong	xiao
print(unpack(table1, 2, 3))     --> peng	yong

六、尾调用

1、作用

尾调用的作用不会使用额外的栈空间(这种实现称为尾调用消除)

因为当被调的函数执行结束后,程序就不再需要返回最初的调用者。

例如以下函数,当调用了 f 函数,f 函数执行完成 g 函数后,不再需要返回 f 函数,而是直接返回到调用 f 函数的地方。

function f(x) 
	x = x + 1; 
	return g(x);
end

由于尾调用函数不会使用栈空间,所以一个程序中能够嵌套的尾调用数量是无限的。

2、如何界定是否为尾调用函数

只有形如 return func(args) 的调用才算。

func 及其参数都可以有复杂的表达式,这些复杂的表达式会被先进行计算完之后才进行调用。

举些例子

下面的代码不是尾调用,因为 f 在返回前,还需要对 g 函数的返回进行结果丢弃。

function f(x) 
	g(x)
end   

下面的代码不是尾调用,因为 g 函数的返回值还需要进一步处理(因为加了括号,会只使用一个返回值,所以也会进行舍弃多余的值),才能成为真正的返回值

return g(x) + 1     
return x or g(x)
return (g(x))

下面的代码是尾调用,因为这里的 x[j] + a*b, i+j 可以先进行计算,x[i].foo 这里也是先获取,然后最后再进调用,所以无需在返回

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

七、写在最后

Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)

公众号搜索 “江澎涌” 可以更快的获取到后续的更新文章

image.png

全部评论

相关推荐

头像
06-28 16:18
已编辑
北京体育大学 测试工程师
面试官周末在外边玩呢,然后约我一边玩,一边儿面试。基本上就是先对我英语能力做了一个考察,然后他做了一下部门的相关的介绍,以及对候选人能力的要求,基本上需要有一定的测试思维,然后沟通能力,然后就是要有开发能力,因为需求迭代的比较快要跟开发一起联动配合做一些自动化的实现,就是有一个相关的需求,能够实现相关的自动化用例需要的能力就是Python和JAVA,可能有一部分的c,c#。一开始是英文做了些介绍,说了1min,然后就是对我自动化能力的了解,然后组里边的一些自动化框架,然后接口自动化的了解,对Python能力的了解。这块儿就是又问了一些数据结构的相关的问题,贪心和动态规划的区别,比如说广度优先遍历和深度优先遍历的区别,然后出了一个场景题,如果我有一个是8byte的字符串,然后有4000个这样的字符串,那么你的bfs,队列要到多长?然后还有就是Python和c#的相关的结合,你可能要用什么样的方法?再就是问我对天津的看法,想不想来?我说之前我在京东的时候还想去天津的京东科技呢。然后面了面人家没要我。然后最后我反问。他是从0~1搭建自动化框架,还是你去完善自动化框架,然后他说正在招一个测试的leader去进行架构设计,然后找几个骨干的自动化测试工程师去实现相关的需求。你最主要的就是开发能力,要可以把一个相关的需求实现到自动化的场景这样子基本的能力要有。目前团队有10个人这样子,但是自动化测试工程师相对来说会少一些,所以说需求的基本上是自动化测试的工程师。然后我再问,可没可能涉及到一些大模型评测,然后一些大模型的微调之类的相关的工作。他说这块儿我们team里也有专门的算法评测团队,这块儿如果你想做的话也可以和他们一起去做,但是微调在现在的阶段是没有的。然后我又问了一下,还有技术面吗?他说如果通过了的话,有我的boss,我的领导再给你面一轮技术面完了再就是HR面一共30分钟面试公司:联想岗位:software&nbsp;QA&nbsp;engineer问题:如上
查看9道真题和解析
点赞 评论 收藏
分享
06-27 15:10
已编辑
门头沟学院 测试工程师
隔了有些时间了,写一写权当复盘。一面(有些记不清了):为什么选择做测试?看你之前实习是软测,为什么选择游测?你认为的游测和软测的区别在哪里?简历中的项目你主要做了什么?平时玩什么游戏?答:什么都玩,最近玩DOTA2多一些。现在比如说要新增一个斯温这个英雄,你要怎么对他进行测试?在测试工作中你的优势和劣势?postman你在工作中具体是怎么用的?可以详细说一下吗?二面(被疯狂拷打场景)一些和一面一样的问题:游测软测的差距?之前做的是软测,为什么不做了?我们聊一聊法环吧,法环的地图你要怎么测试?(然后是根据地图的延伸问题)你认为法环是怎么判断摔死的?如果你在一个低高度的地方卡住了,会死亡吗?他和在高处跌落的差别是什么?你谈到法环地图是无缝的,那你认为无缝是怎么做到的?你真的认为你说的根据任务的推进,法环地图角色出现、死亡与否是属于地图测试的部分吗?就这么点?你认为你说的这些测试点,地图就ok没问题了吗?我们再聊聊DOTA2吧,当年很有名的泉水钩,你认为导致的原因是什么?如果让你来修复这个逻辑,你会怎么修复?如果小精灵带人传送,而tf在传送回去一瞬间勾到了小精灵,那么小精灵是否能正常回城?tf出钩勾到人了,但这个过程被猛犸撞到了,会有怎么样的结果?这个结果你认为是怎么导致的?如果是你说的优先级,那么假设tf的优先级和猛犸冲的优先级是一样的,会有什么样的结果?那按你说的优先级,那这两者就卡着不动了吗?我们再聊一下米波吧,你对米波设计测试用例的话会怎么设计?(后面拓展就不聊了)反问环节,,,,,,,——————————————————————————————————————一面二面的面试官都说面得校招生,但我投的是暑期实习,可能就是按校招生标准来的,场景盛宴,被压力的很难受,还会一直追问,大多问题我现场想出来也答对了,但后续还是挂了,可能是因为没有游戏开发的经验
查看22道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务