Skip to content

可调用体

可调用体Callable,代表一个方法或一个独立函数。

Callable属于值类型,默认值为没有绑定任何对象和方法的空可调用体Callable()

它可以是Object实例中的方法,也可以是用于不同目的的自定义可调用函数。

它可以存储在变量中,也可以传递给其他函数

它最常用于信号回调,类似其他编程语言的函数对象/函数指针

它是在GDScript中灵活地运用观察者模式必不可少的一员,也可以借助可调用体在GDScript实现函数式编程

Callable对象而言,函数是第一类值。如果通过名称引用函数就会自动生成对应的可调用体,因此可以将这种可调用体作为函数的参数传递

gdscript
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及其子类)的成员变量中,因为这可能会导致内存泄漏。

gdscript
var lambda := func(x):
    print(x)

此时变量lambda是一个Callable对象,可以使用Callable.call方法传入必要参数并调用它

gdscript
lambda.call("Hello!") # 打印 "Hello!"

lambda函数不一定非得是匿名的,你也可以为其命名,这样便于代码调试(其名称会显示在调试器上)

除此之外,不允许声明独立的lambda函数,必须将其赋予给一个变量,因此不能在函数内部声明函数

lambda函数也允许同类成员函数一样添加类型提示,也允许返回值,需要显式return

lambda函数也不允许递归调用,即使它是具名的,因此,你可能需要使用迭代的方式来实现

gdscript
# 如果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函数内部更新

gdscript
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: 9
gdscript
var x = 42
var lambda = func (): print(x)
lambda.call() # 打印`42`.
x = "Hello"
lambda.call() # 打印 `42`.

lambda函数也无法给外部的局部变量重新赋值,退出lambda函数体后该变量不会发生改变,因为lambda函数中捕获的版本隐式覆盖了该变量,你可以理解为匿名函数体内部复制了一份相同名称相同值的变量

gdscript
var x = 42
var lambda = func ():
        print(x) # 打印 `42`.
        x = "Hello" # 重新分配 lambda 捕获不会修改外部局部变量 “x”
        print(x) # 打印 `Hello`.
        
lambda.call()
print(x) # 打印 `42`.

如果lambda捕获的变量是引用类型(对象,字典,数组),在lambda函数内部是和外部共享其状态的,在为捕获的变量重新赋值之前,这些变量会共享其内容更改

gdscript
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

gdscript
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

gdscript
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方法也会返回对应的值

gdscript
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方法 调用函数并传入一个数组,数组内是调用该函数所需的所有参数,数组元素数量应与函数所需的参数对应

gdscript
callable.callv(["hello", "world", "!"]) # 输出 "hello world !"

延迟调用

除了使用call直接调用可调用体指向的函数,也支持延迟调用

使用call_deferred方法可以延迟调用可调用体指向的函数,使用方法同call一样,只不过它会在当前帧末尾才调用对应的函数

例如可调用体指向的函数所关联的对象暂时没有初始化完成,此刻使用call立即调用可能会触发异常,而call_deferred会在帧末尾调用,会避免产生初始化未完成造成的异常

绑定参数

除了在调用时传入参数也可以在调用前绑定参数,一般用于连接信号前传入参数

以上面的print_args为例,函数接收三个参数,两个必选参数和一个可选参数

我们使用 bind方法绑定两个参数,bind方法会返回绑定了参数后的Callable副本,由于第三个参数是可选参数,所以我们可以直接call而无需传入任何参数,传入bind里的参数会被追加到call末尾

gdscript
var f = print_args
var f_bound_args = f.bind(1, 2)
# 此时调用不需要再传入参数,因为已经绑定了两个参数,第三个参数是可选参数,输出`1 2`
f_bound_args.call()

除了逐个绑定参数,也支持传入一个数组来绑定参数,bindv方法传入一个数组,该数组是参数列表,bindv方法也会返回当前Callable的副本

gdscript
var f_bound_args = f.bindv([1, 2])

解绑参数

可以在调用前绑定参数,自然也可以在调用前解绑一定数量的参数

unbind方法可以在调用前解绑指定数量用户传入的参数,并返回当前Callable的副本

unbind方法接收一个整数类型的参数,指示用户之后提供最后n 个参数会被忽略,而剩余的参数将被传递给可调用体

它不会影响 unbind 之前已绑定的参数。

忽略的并不是最终构成的参数列表的末尾元素,而是从 unbind() 之后传入的参数中,取其尾部 n 个进行丢弃。

gdscript
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方法与其他类似方法链式调用时参数的修改顺序是从右到左的

gdscript
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 2
  • f.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]

练习

  1. 创建一个 Array[Callable],填入多个 .bind() 了不同按钮名称的回调,依次调用打印不同按钮被点击。

    gdscript
    func on_button_pressed(button_name: String) -> void:
        print(button_name, "pressed!")
  2. 定义多个小函数(如 say_hellosay_goodbyesay_ready),将它们以 Callable 的形式存入一个数组,随机或按顺序调度调用它们。

  3. 定义一个高阶函数 map_array(arr: Array, func: Callable) -> Array,它接收一个数组和一个 Callable,返回每个元素应用该函数后的新数组。 手动实现逻辑,不使用Array.map 例:输入 [1, 2, 3]lambda x: x * 2,返回 [2, 4, 6]

  4. 请编写一个函数 sum_by(array: Array, selector: Callable) -> float, 该函数接收一个数组和一个用于“提取数值”的可调用体 selector。 它会遍历数组中每个元素,将每个元素传入 selector 中执行,得到一个数值,并将这些数值求和返回。

    gdscript
    var 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