可调用体
可调用体Callable,代表一个方法或一个独立函数。
Callable属于值类型,默认值为没有绑定任何对象和方法的空可调用体Callable()
它可以是Object实例中的方法,也可以是用于不同目的的自定义可调用函数。
它可以存储在变量中,也可以传递给其他函数。
它最常用于信号回调,类似其他编程语言的函数对象/函数指针
它是在GDScript中灵活地运用观察者模式必不可少的一员,也可以借助可调用体在GDScript实现函数式编程
就Callable对象而言,函数是第一类值。如果通过名称引用函数就会自动生成对应的可调用体,因此可以将这种可调用体作为函数的参数传递
tween.tween_callback(node.queue_free) # Object 的方法。
tween.tween_callback(array.clear) # 内置类型的方法。
tween.tween_callback(print.bind("Test")) # 全局函数。
arr.map(func(x):return x * 2) # 匿名函数lambda函数
lambda(λ)函数,也称匿名函数,是快速创建Callable的方法之一。
lambda函数是不属于类的函数,区别在于类的函数应该是可复用的代码,而lambda一般是不会复用的。
当我们需要在代码的其他地方多次用到这个函数的逻辑时,那么就应该在类中定义这个函数,否则就是匿名函数作为局部变量
注意:避免将 lambda 函数储存到某些类(如RefCounted及其子类)的成员变量中,因为这可能会导致内存泄漏。
var lambda := func(x):
print(x)此时变量lambda是一个Callable对象,可以使用Callable.call方法传入必要参数并调用它
lambda.call("Hello!") # 打印 "Hello!"lambda函数不一定非得是匿名的,你也可以为其命名,这样便于代码调试(其名称会显示在调试器上)
除此之外,不允许声明独立的lambda函数,必须将其赋予给一个变量,因此不能在函数内部声明函数
lambda函数也允许同类成员函数一样添加类型提示,也允许返回值,需要显式return
lambda函数也不允许递归调用,即使它是具名的,因此,你可能需要使用迭代的方式来实现
# 如果lambda声明在类的成员函数外,匿名函数必须将其分配给变量,如果为其命名且不分配给变量将成为类的成员函数
# 与其这样,不如直接将其提升为类的一个函数,而不是一个Callable变量
var function = func(n: int) -> int:
var result = 1
for i in range(2, n + 1):
result *= i
return result
func _ready() -> void:
var fact = func f(n: int) -> int:
if n == 0:
return 1
else:
return n * f(n - 1) # 错误,lambda函数不允许递归调用自身,fact.call也是不允许的
# 使用迭代的方式实现阶乘
var factorial := func(n: int) -> int: # 添加类型提示
var result = 1
for i in range(2, n + 1):
result *= i
return result # 显式return
print(factorial.call(5))
# 不允许独立的lambda函数,无论是否具名,应将其分配给变量
func(n: int) -> int:
var result = 1
for i in range(2, n + 1):
result *= i
return result闭包捕获
lambda函数可以捕获局部环境,例如迭代变量,局部变量等
lambda函数只会在创建时捕获一次局部变量的值,当变量在外部更改时,其值不会在lambda函数内部更新
var levy_action = []
for i in range(10):
levy_action.append(func(): print("Levy Action: ", i))
for action in levy_action:
action.call() # 输出 Levy Action: 0 ~ Levy Action: 9var x = 42
var lambda = func (): print(x)
lambda.call() # 打印`42`.
x = "Hello"
lambda.call() # 打印 `42`.lambda函数也无法给外部的局部变量重新赋值,退出lambda函数体后该变量不会发生改变,因为lambda函数中捕获的版本隐式覆盖了该变量,你可以理解为匿名函数体内部复制了一份相同名称相同值的变量
var x = 42
var lambda = func ():
print(x) # 打印 `42`.
x = "Hello" # 重新分配 lambda 捕获不会修改外部局部变量 “x”
print(x) # 打印 `Hello`.
lambda.call()
print(x) # 打印 `42`.如果lambda捕获的变量是引用类型(对象,字典,数组),在lambda函数内部是和外部共享其状态的,在为捕获的变量重新赋值之前,这些变量会共享其内容更改
var a = []
var lambda = func ():
a.append(1)
print(a) # 打印 `[1]`.
a = [2] # 重新分配 lambda 捕获不会修改外部局部变量 “a”
print(a) # 打印 `[2]`.
lambda.call()
print(a) # 打印 `[1]`.创建
除了通过lambda函数快速创建Callable,还可以使用构造函数或直接引用函数名称来创建基于某一对象的Callable
func print_args(arg1, arg2, arg3 = ""):
prints(arg1, arg2, arg3)
# 使用构造函数创建`Callable`, 这个可调用体指向当前类实例的"print_args"方法
var callable = Callable(self, "print_args")Callable具有三个构造函数:
Callable Callable(): 构造一个空的可调用体,没有绑定对象和方法Callable Callable(from: Callable):构造给定可调用体的副本Callable Callable(object: Object, method: StringName):创建指向object对象实例中名为method方法的可调用体,仅限Object及其子类型。
上面的第三个构造函数仅限用于对象类型,对于内置的Variant类型,需要使用静态方法Callable.create,来创建指向该变体类型中指定名称的方法的可调用体
但是在大多数情况下,不需要显式使用构造函数来创建Callable
可以直接引用大多数对象的方法名称直接创建指向该方法的Callable
extends Node
var arr = [1, 2, 3]
var dict = {"hello":"world"}
var callable = print_args # 指向类中创建的方法
var free = queue_free # 指向 Node 的方法,来自于父类
var clear_arr = arr.clear # 指向 数组 实例的方法
# var clear_dict = dict.clear # 不行,由于字典存在点语法,"clear"被视为一个键
var clear_dict = Callable.create(dict, "claer") # 使用create静态方法创建Callable,指向字典的clear方法
func print_args(arg1, arg2, arg3 = ""):
prints(arg1, arg2, arg3)字典类型是个例外,由于字典存在通过点语法来访问某个键,因此必须使用create静态方法才能创建指向字典实例的某个方法的Callable
调用与传参
该部分的所有
Callable所指向的函数均为上文出现的print_args方法
调用传参
调用Callable需要在一个Callable实例上调用 call方法,并传入一定的参数以匹配该Callable所指向方法的签名,否则将会触发一个运行时错误
如果Callable指向的方法具有返回值,则call方法也会返回对应的值
func print_args(arg1, arg2, arg3 = ""):
prints(arg1, arg2, arg3)
var callable = print_args
callable.call("hello", "world") # 输出“hello world ”。
callable.call(Vector2.UP, 42, callable) # 输出“(0.0, -1.0) 42 Node(文件名.gd)::print_args”
callable.call("invalid") # 无效调用,应当至少有 2 个参数,最多 3 个参数。除了使用call方法调用函数并逐个传递参数外,还可以使用 callv方法 调用函数并传入一个数组,数组内是调用该函数所需的所有参数,数组元素数量应与函数所需的参数对应
callable.callv(["hello", "world", "!"]) # 输出 "hello world !"延迟调用
除了使用call直接调用可调用体指向的函数,也支持延迟调用
使用call_deferred方法可以延迟调用可调用体指向的函数,使用方法同call一样,只不过它会在当前帧末尾才调用对应的函数
例如可调用体指向的函数所关联的对象暂时没有初始化完成,此刻使用call立即调用可能会触发异常,而call_deferred会在帧末尾调用,会避免产生初始化未完成造成的异常
绑定参数
除了在调用时传入参数也可以在调用前绑定参数,一般用于连接信号前传入参数
以上面的print_args为例,函数接收三个参数,两个必选参数和一个可选参数
我们使用 bind方法绑定两个参数,bind方法会返回绑定了参数后的Callable副本,由于第三个参数是可选参数,所以我们可以直接call而无需传入任何参数,传入bind里的参数会被追加到call末尾
var f = print_args
var f_bound_args = f.bind(1, 2)
# 此时调用不需要再传入参数,因为已经绑定了两个参数,第三个参数是可选参数,输出`1 2`
f_bound_args.call()除了逐个绑定参数,也支持传入一个数组来绑定参数,bindv方法传入一个数组,该数组是参数列表,bindv方法也会返回当前Callable的副本
var f_bound_args = f.bindv([1, 2])解绑参数
可以在调用前绑定参数,自然也可以在调用前解绑一定数量的参数
unbind方法可以在调用前解绑指定数量用户传入的参数,并返回当前Callable的副本
unbind方法接收一个整数类型的参数,指示用户之后提供的最后n 个参数会被忽略,而剩余的参数将被传递给可调用体
它不会影响 unbind 之前已绑定的参数。
忽略的并不是最终构成的参数列表的末尾元素,而是从 unbind() 之后传入的参数中,取其尾部 n 个进行丢弃。
f.unbind(1).call(1, 2, 3) # 调用 print_args(1, 2),解绑了call的1个参数
f.bind(1, 2).unbind(3).call(4, 5, 6, 7) # 调用 print_args(4, 1, 2),解绑了call的3个参数
f.unbind(2).bind(1, 2).call(3, 4) # 调用 print_args(3, 4),解绑了bind的两个参数
f.unbind(4).bind(1, 2).call(5, 6, 3, 4) # 调用 print_args(5, 6),解绑了bind的两个参数和call的两个参数参数列表修改顺序
需要注意的是bind/unbind方法与其他类似方法链式调用时参数的修改顺序是从右到左的
f.bind(1).bind(2).call(3) # 输出 3 2 1
f.bind(1, 2).call(3) # 输出 3 1 2
f.bind(1, 2).bind(3).call() # 输出 3 1 2
f.unbind(4).bind(1, 2).call(5, 6, 3, 4) # 输出 5 6
f.bind(1, 2).unbind(3).call(4, 5, 6, 7) # 输出 4 1 2f.bind(1).bind(2).call(3)call(3):追加参数[3]bind(2):追加参数[3, 2]bind(1):追加参数[3, 2, 1]→ 最终参数为[3, 2, 1],顺序为从右到左
f.bind(1, 2).call(3)call(3):追加参数[3]bind(1, 2):追加参数[3, 1, 2]→ 最终参数为[3, 1, 2]
f.bind(1, 2).bind(3).call()call():无参数bind(3):追加参数[3]bind(1, 2):追加参数[3, 1, 2]→ 最终参数为[3, 1, 2]
f.unbind(4).bind(1, 2).call(5, 6, 3, 4)call(5, 6, 3, 4):追加参数[5, 6, 3, 4]bind(1, 2):追加参数[5, 6, 3, 4, 1, 2]unbind(4):从unbind之后的参数中移除最后 4 个 → 剩下[5, 6]→ 最终参数为[5, 6]
f.bind(1, 2).unbind(3).call(4, 5, 6, 7)call(4, 5, 6, 7):追加参数[4, 5, 6, 7]unbind(3):移除最后 3 个参数 → 剩下[4]bind(1, 2):追加参数[4, 1, 2]→ 最终参数为[4, 1, 2]
练习
创建一个
Array[Callable],填入多个.bind()了不同按钮名称的回调,依次调用打印不同按钮被点击。gdscriptfunc on_button_pressed(button_name: String) -> void: print(button_name, "pressed!")定义多个小函数(如
say_hello、say_goodbye、say_ready),将它们以Callable的形式存入一个数组,随机或按顺序调度调用它们。定义一个高阶函数
map_array(arr: Array, func: Callable) -> Array,它接收一个数组和一个Callable,返回每个元素应用该函数后的新数组。 手动实现逻辑,不使用Array.map例:输入[1, 2, 3]和lambda x: x * 2,返回[2, 4, 6]请编写一个函数
sum_by(array: Array, selector: Callable) -> float, 该函数接收一个数组和一个用于“提取数值”的可调用体selector。 它会遍历数组中每个元素,将每个元素传入selector中执行,得到一个数值,并将这些数值求和返回。gdscriptvar items = [ {"name": "apple", "price": 3.5}, {"name": "banana", "price": 2.0}, {"name": "peach", "price": 4.0}, ] var total_price = sum_by(items, func (item): return item["price"]) print(total_price) # 应输出 9.5