局部变量
「松饼人物编辑器:从入门到欧耶」系列教程(四)
上一讲的习题参考答案
1) 抄代码运行即可验证
2) 抄代码运行即可验证
3) 一个参考:
function repeat3(str)
return str .. str .. str
end
print(repeat3("嗷"))
脑容量问题
写代码时,我们会不断地「起名字」 —— 有时给变量起名,有时又给函数起名。 起的名字越多,记忆的负担也就越大 —— 我们需要记住,这个函数是干什么的,那个变量又是干什么的。 有时候,可能会由于疏忽,把某个已经有了某个用途名字, 当成一个新名字用到另一个用途上 —— 两块功能交替使用一个名字,结果出了一堆奇怪的错误。
举个粟子:
function repeat2(x)
a = x .. x
return a
end
a = "哇"
b = repeat2("哈")
print(a .. b)
首先,定义一个把字符串重复 2 次的函数repeat2
。
然后,a
是「哇」,b
是重复两次的「哈」,打印a .. b
。
结果会输出什么?
肯定是「哇哈哈」啊,这种局面还看不懂吗?
(测试见输出「哈哈哈哈」)
…枼?…别啊?!哎~!呀~!这解说不下去了,哎呀这!!呃啊~~
(为什么会变成这样呢……)
要想修正这个问题,要么repeat2
里别用a
,要么外面别用a
。
总之就是只能有一个地方能用a
。
不管代码写得多长,都得记住a
是给哪一段代码用的 ——
除了那段代码,其它地方都不能用a
。
这就坑爹了。你得记住每一个变量名都是用在哪里的,不论代码有多长。 这是想把人脑袋撑爆啊x
所以,局部变量大法好。
关键字 local
用local
大法,将刚才的代码改写如下:
function repeat2(x)
local a = x .. x
return a
end
a = "哇"
b = repeat2("哈")
print(a .. b)
测试见输出「哇哈哈」。
对一个变量赋值前,前面加上local
,
表明被赋值的变量是一个新创建的「局部变量」。
局部变量,就是只在局部有效的变量。 比如你在麻吧里说「老板」,指的肯定就是照老板, 不是其它的什么老板。 这个时候,「麻吧」就是一个「局部」,在这个局部里,「老板」特指这个局部里的照老板。
在repeat2
函数中,由于a
被声明成了一个局部变量,
在repeat2
内的a
指的都是repeat2
里的这个a
,
跟外面a = "哇"
的那个a
就完全没关系了。
作用域
局部变量从对应的local
那一行开始可以用,
到声明了这个local
所在的「块」的结束就不能再用了。
所谓的「块」,可以是函数,也可以是if
语句,也可以是以后介绍的其它东西。
习惯上,每进入一个新的「块」,都会增加一级缩进。
在一个「块」里使用一个名字时, 如果这个「块」里有叫这个名字的局部变量, 那么这个名字指的就是这个局部变量。 如果这个「块」里没有叫这个名的局部变量, 这个名字指的就是外面的「块」里的局部变量。 外面的「块」还没有,就到外面的外面的「块」里去找。 如果连最外面的「块」里也没有,这个名字指的就是全局变量。
举个粟子:
function f()
print(a) -- A
local a = 1
print(a) -- B
if true then
print(a) -- C
local a = 2
print(a) -- D
end
print(a) -- E
end
f()
以上代码先后输出 nil, 1, 1, 2, 1。
从--
到行尾的文字叫做「注释」,会被机器无视掉,是写给人看的。
这里我们注释标出了 A, B, C, D, E 五个行,以便于说明。
执行到 A 行的时候,当前代码所在的「块」就是函数f
。
这个「块」里还没有局部变量a
,所以这个a
指的是外面的a
。
外面也没有a
,所以这里的a
是个全局变量。
我们没有碰过全局变量a
,所以此行输出 nil。
执行到 B 行的时候,当前的「块」里已经有局部变量a
了,值为 1,所以输出1。
执行到 C 行的时候,if
语句开启了一个新的「块」。
当前「块」里没有a
,所以这里的a
指的是外面的「块」里的a
,
也就是 1。
执行到 D 行的时候,if
「块」里也有了a
,值为 2。
所以此时的a
指的就是这个if
里新建的a
。
到了 E 行,已经退出了if
「块」,所以if
里的那个a
已经没了。
现在的a
指的是函数「块」里的a
,也就是 1。
疯狂使用局部变量
局部变量不但可以节约人的脑容量,
还可以提高代码的运行速度(原理不展开)。
所以local
关键字应该能加就加。
另外,局部变量的声明要尽可能「晚」出现。 举个粟子:
function f()
local a = 1
if true then
print(a)
end
end
function f()
if true then
local a = 1
print(a)
end
end
上面的两段代码所做的事情是相同的,但第二段代码读起来更舒服。
第一段代码中,if
外面明明没有用到a
,却把a
放到了if
外面,愣是增加了理解的难度。
函数的参数
函数的参数虽然不带local
字样,但都是局部变量,只在函数内有效。
调用函数的时候,传的是「值」 —— 值会被复制一份,对复制品的操作影响不到原品。
function f(x)
x = 2
end
a = 1
f(a)
print(a)
上面的代码输出 1,而不是 2。在调用f(a)
的时候,a
的「值」
—— 也就是 1 —— 被复制了一份给局部变量x
。
随后x
这个局部变量被改成了 2,但这跟a
有什么关系?所以最终输出 1。
function f(a)
a = 2
end
a = 1
f(a)
print(a)
上面的代码仍然输出 1。参数的名字被改成了a
,
但它也是一个仅在函数内存在的局部变量,跟外面的那个a
没关系。
上值与闭包
和其它的变量一样,函数也是可以在局部随时创建的:
function f()
local function g()
print("a")
end
g()
end
f()
在函数的定义内,可以访问外部的局部变量。 函数内使用到的外部的局部变量,叫「上值」。
function f()
local str = "a"
local function g()
print(str) -- str是g的一个上值
end
g()
end
f()
我们可以在函数里定义新函数并返回出去,达到用函数去创建函数的目的:
function make()
local str = "a"
local function g()
print(str)
end
return g -- 返回一个函数g
end
local h = make() -- 通过make函数创建h函数
h() -- 调用h函数
每执行一次function
与end
之间的部分,
都会产生一个新的函数。举个粟子:
function make()
local i = 1
local function g(x)
print(i)
i = x
end
return g
end
local h1 = make()
local h2 = make()
h1(233)
h2(666)
h1(0)
h2(0)
测试见依次输出 1, 1, 233, 666。
这可以证明h1
和h2
里面的上值i
虽然名字相同,
但并不是同一个变量。
这是因为,h1
与h2
是在两次make
的调用中创建的 ——
每次调用make
,都会创建一个新的局部变量i
。
随后,又创建新的函数g
,这个新的函数引用了刚刚新建的i
。
所以h1
与h2
中的i
指的就是两个不同的变量。
可见,一个函数不仅代表一堆指令,还可以拥有只属于自己的一堆上值,
就好像带着一个小包包一样。
像这种带着包的函数,通常叫「闭包」。
上面的例子中,函数h1
和h2
的定义虽然相同,但它们是两个不同的闭包。
松饼的官方范例与之后的教程中,闭包并不常用; 对于人物编辑而言,闭包也不是刚需。 但为了避免写出 bug,或产生奇怪的误解, 上值与闭包的基本概念还是必须要掌握的。
练习题
1) 判断题:「局部变量是给笨蛋用的,记忆力好的人不需要用局部变量。」
2) 有些人喜欢把所有的局部变量声明写在函数开头,觉得这样很「清晰」,
对此你怎么看?
3) 完成make
函数的定义,使代码先后输出「嗷」、「嗷嗷」、「嗷嗷嗷」。
function make(unit)
-- TODO 填写实现
end
ao = make("嗷")
ao()
ao()
ao()
下一讲:userdata