「松饼人物编辑器:从入门到欧耶」系列教程(四)

上一讲的习题参考答案

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函数

每执行一次functionend之间的部分, 都会产生一个新的函数。举个粟子:

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。 这可以证明h1h2里面的上值i虽然名字相同, 但并不是同一个变量。 这是因为,h1h2是在两次make的调用中创建的 —— 每次调用make,都会创建一个新的局部变量i。 随后,又创建新的函数g,这个新的函数引用了刚刚新建的i。 所以h1h2中的i指的就是两个不同的变量。

可见,一个函数不仅代表一堆指令,还可以拥有只属于自己的一堆上值, 就好像带着一个小包包一样。 像这种带着包的函数,通常叫「闭包」。 上面的例子中,函数h1h2的定义虽然相同,但它们是两个不同的闭包。

松饼的官方范例与之后的教程中,闭包并不常用; 对于人物编辑而言,闭包也不是刚需。 但为了避免写出 bug,或产生奇怪的误解, 上值与闭包的基本概念还是必须要掌握的。


练习题

1) 判断题:「局部变量是给笨蛋用的,记忆力好的人不需要用局部变量。」
2) 有些人喜欢把所有的局部变量声明写在函数开头,觉得这样很「清晰」, 对此你怎么看?
3) 完成make函数的定义,使代码先后输出「嗷」、「嗷嗷」、「嗷嗷嗷」。

function make(unit)
  -- TODO 填写实现
end

ao = make("嗷")
ao()
ao()
ao()

下一讲:userdata