本帖最后由 囿里有条小咸鱼 于 2024-12-23 22:58 编辑
17. GDScript中lambda表达式的一些坑 很多刚上手gdscript的开发者一不小心便容易掉进lambda表达式(一般是匿名函数)的坑里。试看下面这段lambda表达式:
- var a = 1
- var u = func():
- print("First a: %s" % a)
- a = 2
- print("Second a: %s" % a)
复制代码 估计会有小伙伴认为这个代码中最后一行会输出Real a: 3,对吧?那么就来看看实际上对不对吧:
- First a: 1
- Second a: 2
- Real a: 1
复制代码 欸!发现了吗?在u所指向的匿名函数外的print居然输出的是没有经过修改的a值,这是怎么一回事呢?
实际上,在GDScript中,lambda表达式也像普通函数一样有一个范围,在这个范围内去捕获对应的数据。这个范围就叫做捕获作用域(Capture scope),个人习惯简称为捕获域,只有在这个作用范围内的所有信息才会被lambda表达式所捕获。对于lambda表达式而言,其捕获域是从其函数声明开始往上追溯,直到追溯到lambda表达式自身所在作用域的开头(如果在一个函数内,则为该函数的作用域开头)。lambda表达式不会对其声明及定义后面的内容进行捕获。也就是说,下面这行代码会直接报错:
- var q = func():
- print(s) # 报错,在作用域内找不到标识符s
- var s = 1 # 在lambda表达式后面的声明不会被该lambda表达式所捕获
复制代码 lambda函数的捕获域还有一个特点:如果对捕获域内所声明的数据进行修改,那么该修改只会在其lambda函数体本身及其更子级的lambda表达式(也就是嵌套的子lambda表达式)内生效,一旦超出该lambda函数体(的母体),则对被捕获的数据的修改均会回退至其被lambda表达式捕获前的状态,也因此就出现了前文当中的情况。
此外还有一点,lambda表达式只会对其捕获域内的数据进行一次捕获操作。一旦该数据被捕获,那么该数据在lambda表达式中的初始形式也就确定了。看不懂什么意思?那就上代码:
- var q = 2
- var s = func(): print(q)
- s.call() # 结果为2
- q = 4
- s.call() # 结果仍然为2
复制代码 不知道各位小伙伴看明白了没有:我在lambda表达式前声明了一个q变量,存入整数数据2。然后又声明了s,指向一个lambda表达式,在该lambda表达式里打印q。根据刚才提到的内容,此时q在该lambda函数的捕获域内,其在lambda函数内的初始状态也就随之固定了。这个时候,无论你在lambda函数声明与定义后进行何种修改,只要不在lambda函数体内对q进行修改,print()函数打印出来的q就依旧是其在被lambda表达式捕获前的q,而非在lambda表达式定义后面发生变化后的q。换句话说,lambda表达式的作用环境在其声明时进行初始化,此后,其内部的作用环境与外部的作用环境就完全隔离了。
不过刚才这些都是对lambda表达式声明前的非引用类型进行操作,但如果是对引用类型进行操作呢?比如数组、字典、对象。。。
我们先需要区分两个概念,赋值和赋引用:
- 赋值,主要是对值类型的数据而言的,比如int, float, bool这些,如果“=”右侧接着一个值类型的数据,则表示将该数值传递给一个变量。
- 赋引用,对于引用类型而言,比如数组、字典、对象等复杂数据结构,我们往往会将其引用进行传递。如果“=”右侧接着一个引用类型的数据,则表示将对该引用类型数据的引用传递给左侧的变量。
也就是说,对于引用类型的数据而言,“=”实际上更偏向于把一串数据的地址(数据所在的地方)传递给左侧的变量来管理了,我们通常就会说“xxx变量指向/引用xxx数组/字典/对象”,因为比起值类型,引用类型的“=”具有非常强烈的指向性。
我们再回到lambda表达式里,实际上,lambda表达式依然会捕获其捕获域内的引用类型,但捕获的是这些引用数据类型本身,而不是变量本身。然而,基于引用类型传递引用的这一特性,你依旧可以在lambda表达式内对捕获域内的引用类型的数据进行操作,同时,在该lambda函数的定义后面,该引用类型的数据在lambda内所产生的修改效果依旧有效:
- var u = [1]
- var r = func(): u.append(2)
- u.append(3)
- print(u) # 结果为[1,2,3]
复制代码 但需要特别注意以下的例子:
- var u = [1]
- var r = func(): u = [2]
- u.append(3)
- print(u) # 结果为[1,3]
复制代码 欸,为啥变成[1,3]了,为啥不是[2,3]呢?
实际上,[x, y, ...]的操作相当于构造了一个新的数组(字典类同),也就是相当于Array(x, y, ...),此时,lambda函数体内的u = [2]实际上是让u指向了一个新的Array(2)的数组。而lambda表达式实际上捕获的是u所指向的对象,而根据前面所提到的lambda表达式捕获域及其捕获数据的特性,u = [2]很明显是在lambda函数的作用域内把u = [1]这个操作进行了重定义的。一离开lambda函数的函数体,u = [2]这个操作也就会回退回其被捕获前的状态,也就是u = [1]。
那为啥append()就行呢?要知道,append()是直接通过u找到u所指向的数组,对数组本身进行操作的,根据前文所提到的引用类型数据的特点,这样的操作所带来的影响依旧可以带出lambda函数的函数体,因此append()是可以产生上述影响的。
以上就是有关gdscript lambda表达式的一些坑,希望能帮助更多小伙伴防止踩雷。
|