33. 函数是一等公民(头等函数)
现在简单概括一下本教程讨论的内容:
- 什么是头等函数?
- 匿名函数
- 用户自定义的函数类型
- 高阶函数
- 把函数作为参数,传递给其它函数
- 在其它函数中返回函数
- 闭包
- 头等函数的实际用途
什么是头等函数?
支持头等函数(First Class Function)的编程语言,可以把函数赋值给变量,也可以把函数作为其它函数的参数或者返回值。Go 语言支持头等函数的机制。
本教程我们会讨论头等函数的语法和用例。
匿名函数
我们来编写一个简单的示例,把函数赋值给一个变量。
1 | package main |
在上面的程序中,我们将一个函数赋值给了变量 a
(第 8 行)。这是把函数赋值给变量的语法。你如果观察得仔细的话,会发现赋值给 a
的函数没有名称。由于没有名称,这类函数称为匿名函数(Anonymous Function)。
调用该函数的唯一方法就是使用变量 a
。我们在下一行调用了它。a()
调用了这个函数,打印出 hello world first class function
。在第 12 行,我们打印出 a
的类型。这会输出 func()
。
运行该程序,会输出:
1 | hello world first class function |
要调用一个匿名函数,可以不用赋值给变量。通过下面的例子,我们看看这是怎么做到的。
1 | package main |
在上面的程序中,第 8 行定义了一个匿名函数,并在定义之后,我们使用 ()
立即调用了该函数(第 10 行)。该程序会输出:
1 | hello world first class function |
就像其它函数一样,还可以向匿名函数传递参数。
1 | package main |
在上面的程序中,我们向匿名函数传递了一个字符串参数(第 10 行)。运行该程序后会输出:
1 | Welcome Gophers |
用户自定义的函数类型
正如我们定义自己的结构体类型一样,我们可以定义自己的函数类型。
1 | type add func(a int, b int) int |
以上代码片段创建了一个新的函数类型 add
,它接收两个整型参数,并返回一个整型。现在我们来定义 add
类型的变量。
我们来编写一个程序,定义一个 add
类型的变量。
1 | package main |
在上面程序的第 10 行,我们定义了一个 add
类型的变量 a
,并向它赋值了一个符合 add
类型签名的函数。我们在第 13 行调用了该函数,并将结果赋值给 s
。该程序会输出:
1 | Sum 11 |
高阶函数
高阶函数(Hiher-order Function)定义为:满足下列条件之一的函数:
- 接收一个或多个函数作为参数
- 返回值是一个函数
针对上述两种情况,我们看看一些简单实例。
把函数作为参数,传递给其它函数
1 | package main |
在上面的实例中,第 7 行我们定义了一个函数 simple
,simple
接收一个函数参数(该函数接收两个 int
参数,返回一个 a
整型)。在 main
函数的第 12 行,我们创建了一个匿名函数 f
,其签名符合 simple
函数的参数。我们在下一行调用了 simple
,并传递了参数 f
。该程序打印输出 67。
在其它函数中返回函数
现在我们重写上面的代码,在 simple
函数中返回一个函数。
1 | package main |
在上面程序中,第 7 行的 simple
函数返回了一个函数,并接受两个 int
参数,返回一个 int
。
在第 15 行,我们调用了 simple
函数。我们把 simple
的返回值赋值给了 s
。现在 s
包含了 simple
函数返回的函数。我们调用了 s
,并向它传递了两个 int 参数(第 16 行)。该程序输出 67。
闭包
闭包(Closure)是匿名函数的一个特例。当一个匿名函数所访问的变量定义在函数体的外部时,就称这样的匿名函数为闭包。
看看一个示例就明白了。
1 | package main |
在上面的程序中,匿名函数在第 10 行访问了变量 a
,而 a
存在于函数体的外部。因此这个匿名函数就是闭包。
每一个闭包都会绑定一个它自己的外围变量(Surrounding Variable)。我们通过一个简单示例来体会这句话的含义。
1 | package main |
在上面程序中,函数 appendStr
返回了一个闭包。这个闭包绑定了变量 t
。我们来理解这是什么意思。
在第 17 行和第 18 行声明的变量 a
和 b
都是闭包,它们绑定了各自的 t
值。
我们首先用参数 World
调用了 a
。现在 a
中 t
值变为了 Hello World
。
在第 20 行,我们又用参数 Everyone
调用了 b
。由于 b
绑定了自己的变量 t
,因此 b
中的 t
还是等于初始值 Hello
。于是该函数调用之后,b
中的 t
变为了 Hello Everyone
。程序的其他部分很简单,不再解释。
该程序会输出:
1 | Hello World |
头等函数的实际用途
迄今为止,我们已经定义了什么是头等函数,也看了一些专门设计的示例,来学习它们如何工作。现在我们来编写一些实际的程序,来展现头等函数的实际用处。
我们会创建一个程序,基于一些条件,来过滤一个 students
切片。现在我们来逐步实现它。
首先定义一个 student
类型。
1 | type student struct { |
下一步是编写一个 filter
函数。该函数接收一个 students
切片和一个函数作为参数,这个函数会计算一个学生是否满足筛选条件。写出这个函数后,你很快就会明白,我们继续吧。
1 | func filter(s []student, f func(student) bool) []student { |
在上面的函数中,filter
的第二个参数是一个函数。这个函数接收 student
参数,返回一个 bool
值。这个函数计算了某一学生是否满足筛选条件。我们在第 3 行遍历了 student
切片,将每个学生作为参数传递给了函数 f
。如果该函数返回 true
,就表示该学生通过了筛选条件,接着将该学生添加到了结果切片 r
中。你可能会很困惑这个函数的实际用途,等我们完成程序你就知道了。我添加了 main
函数,整个程序如下所示:
1 | package main |
在 main
函数中,我们首先创建了两个学生 s1
和 s2
,并将他们添加到了切片 s
。现在假设我们想要查询所有成绩为 B
的学生。为了实现这样的功能,我们传递了一个检查学生成绩是否为 B
的函数,如果是,该函数会返回 true
。我们把这个函数作为参数传递给了 filter
函数(第 38 行)。上述程序会输出:
1 | [{Samuel Johnson B USA}] |
假设我们想要查找所有来自印度的学生。通过修改传递给 filter
的函数参数,就很容易地实现了。
实现它的代码如下所示:
1 | c := filter(s, func(s student) bool { |
请将该函数添加到 main
函数,并检查它的输出。
我们最后再编写一个程序,来结束这一节的讨论。这个程序会对切片的每个元素执行相同的操作,并返回结果。例如,如果我们希望将切片中的所有整数乘以 5,并返回出结果,那么通过头等函数可以很轻松地实现。我们把这种对集合中的每个元素进行操作的函数称为 map
函数。相关代码如下所示,它们很容易看懂。
1 | package main |
该程序会输出:
1 | [25 30 35 40 45] |