www.zhblog.net

匿名函数与闭包


除了作为值传递之外,Go 的函数还可以是匿名函数,且具有闭包(Closure)的特性,由于 Go 具有指针,在理解闭包时反而容易一些了。

匿名函数

在〈一级函数〉中,我们看过函数可作为值传递的一个应用是,可将函数传入另一函数作为回调(Callback),除了传递具名的函数之外,有时会想要临时创建一个函数进行传递,例如:

package main

import "fmt"

type Predicate = func(int) bool

func filter(origin []int, predicate Predicate) []int {
    filtered := []int{}
    for _, elem := range origin {
        if predicate(elem) {
            filtered = append(filtered, elem)
        }
    }
    return filtered
}

func main() {
    data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println(filter(data, func(elem int) bool {
        return elem > 5
    }))
    fmt.Println(filter(data, func(elem int) bool {
        return elem <= 6
    }))
}

这个函数与〈一级函数〉中最后一个范例的作用相同,不过这次传递了匿名函数给filter,可以看到,匿名函数可使用func创建,同样必须指定参数与返回值类型。

在 Go 中,不允许在函数中又定义函数,例如,以下是不允许的:

func funcA() {
    func funcB() {
        ...
    }
    ...
}

这会出现 “nested func not allowed” 的错误,然而,你可以创建匿名函数,然后将之指定给某个变量:

func funcA() {
    funcB := func() {
       ...
    }
    ...
}

你也可以在函数中创建匿名函数,并将之返回:

package main

import "fmt"

type Func1 = func(int) int

func funcA() Func1 {
    x := 10
    return func(n int) int {
        return x + n
    }
}

func main() {
    fmt.Println(funcA()(2)) // 12
}

在上面的范例中,执行funcA会返回一个函数,这个返回的函数会将接受的实参指定给参数n,并与x的值进行相加,因此最后显示结果为 12。

闭包

可以在函数中创建匿名函数,引发了一个有趣的事实,先来看个例子:

package main

import "fmt"

type Consumer = func(int)

func forEach(elems []int, consumer Consumer) {
    for _, elem := range elems {
        consumer(elem)
    }
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    sum := 0
    forEach(numbers, func(elem int) {
        sum += elem
    })
    fmt.Println(sum) // 15
}

乍看之下,似乎有点像是:

package main

import "fmt"

type Consumer = func(int)

func forEach(elems []int, consumer Consumer) {
    for _, elem := range elems {
        consumer(elem)
    }
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    sum := 0
    for _, elem := range numbers {
        sum += elem
    }
    fmt.Println(sum) // 15
}

然而意义完全不同。在使用forEach函数的范例中,sum变量被匿名函数包覆并传入forEach之中,在forEach执行循环的过程中,每次调用传入的函数(被consumer参考),就会改变sum的值,因此,最后得到的是累加后的值 15。

实际上,使用forEach函数的范例中,创建了一个闭包,闭包本质上就是一个匿名函数,sum变量被闭包包覆,让sum变量可以存活于闭包的范畴中,其实,更之前从funcA返回函数的范例中,也创建了闭包,funcAx局部变量被闭包包覆,因此,你执行返回的函数时,即使funcA已执行完毕,x变量依然是存活着在返回的闭包范畴中,所以,你指定的实参总是会与x的值进行相加。

重点在于,闭包将变量本身关闭在自己的范畴中,而不是变量的值,可以用以下这个范例来做个示范:

package main

import "fmt"

type Getter = func() int
type Setter = func(int)

func x_getter_setter(x int) (Getter, Setter) {
    getter := func() int {
        return x
    }
    setter := func(n int) {
        x = n
    }
    return getter, setter
}

func main() {
    getX, setX := x_getter_setter(10)

    fmt.Println(getX()) // 10
    setX(20)
    fmt.Println(getX()) // 20
}

x_getter_setter来说,x参数也是变量,x_getter_setter返回了两个匿名函数,这两个匿名函数都形成了闭包,将x变量关闭在自己的范畴中,因此,你使用了setX(20)改变了x的值,使用getX()时获取的值,就会是修改后的值。

闭包与指针

如果你写过 JavaScript,对于方才的范例,应该不会陌生,也因为 JavaScript 的普及,现在开发者多半对闭包不会觉得神秘难解了,而对于「闭包将变量本身关闭在自己的范畴中,而不是变量的值」,也比较了解其应用所在。

由于 Go 语言有指针,我们可以将指针的值显示出来,这代表着变量的地址值,来看看被闭包关闭的变量,到底是怎么一回事好了:

package main

import "fmt"

type Getter = func() int
type Setter = func(int)

func x_getter_setter(x int) (Getter, Setter) {
    fmt.Printf("the parameter :\tx (%p) = %d\n", &x, x)

    getter := func() int {
        fmt.Printf("getter invoked:\tx (%p) = %d\n", &x, x)
        return x
    }
    setter := func(n int) {
        x = n
        fmt.Printf("setter invoked:\tx (%p) = %d\n", &x, x)
    }
    return getter, setter
}

func main() {
    getX, setX := x_getter_setter(10)

    fmt.Println(getX())
    setX(20)
    fmt.Println(getX())
}

这个范例与前一个范例类似,只不过调用函数时,都会显示x变量的地址值与存储值,一个执行结果是:

the parameter : x (0x104382e0) = 10
getter invoked: x (0x104382e0) = 10
10
setter invoked: x (0x104382e0) = 20
getter invoked: x (0x104382e0) = 20
20

看到了吗?显示的变量的地址值都是相同的,闭包将变量本身关闭在自己的范畴中,而不是变量的值,就是这么一回事。


展开阅读全文

评论

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 心情