高阶函数就是以另一个函数作为参数或者返回值的函数。在kotlin中,函数用lambda或者函数引用来表示。因此任何以lambda或者函数引用作为参数的函数,或者返回值为lambda或函数引用的函数,或者两者都满足的函数都是高阶函数。比如标准库中的filter函数就将一个判断式函数作为参数。
在上面这个例子中,编译器推导出sum和action两个变量具有函数类型,下图为它们的显式声明
sum有两个Int型参数和Int型返回值的函数,而action是一个没有参数和返回值的函数
声明函数类型,需要将函数参数类型放在括号中,紧接着是一个箭头和函数的返回类型
Unit
类型用于表示函数不返回任何有用的值。在声明一个普通函数时,Unit类型可以忽略,但是一个函数类型声明总是需要一个显式的返回类型,所以这种场景下Unit不能省略。注意在almbda表达式{x,y->x+y}中是如何省略xy的类型的,因为它们的类型已经在函数类型的变量声明部分指定乐,不需要在lambda本身的定义中重复声明
函数类型的返回值也可以标记为可空类型
也可以定义一个函数类型的可空变量,为了明确表示是变量本身为空,而不是函数类型的返回类型可空,需要将整个函数类型的定义包含在括号内并在括号后面添加一个问号
若省略括号则声明的是一个返回值可空的函数类型,而不是一个可空的函数类型的变量
可以为函数类型声明中的参数指定名字
可以使用API中提供的参数名字作为lambda参数的名字,也可以改变参数的名字
参数名称不会影响类型的匹配,当你声明一个lambda时,不必使用和函数类型声明中一摸一样的参数名称
定义一个简单高阶函数
调用作为参数的函数和调用普通函数的语法一样
filter函数:以一个判断式作为参数
实现一个简单版本的filter
函数
filter函数检查字符串的每一个字符是否满足这个判断式,满足则把它添加到包含结果的StringBuilder中。如果我们单步调试上面的例子,我们会看到程序在filter函数体和传递给函数的lambda之间来回移动执行,因为函数会处理输入列表当中的每一个元素
原理:函数类型被声明为普通的接口:一个函数类型的变量是FunctionN
接口的一个实现。kotlin标准库定义了一系列的接口,这些接口对应于不同参数数量的函数:Function0
没有参数的函数Function1
一个参数的函数。每个接口定义了一个invoke
方法,调用这个方法就会执行函数。一个函数类型的变量就是实现了对应的FunctionN接口的实现类的实例,实现类的invoke方法包含了lambda函数体。在java8中的lambda会被自动转换为函数类型的值。在java中可以很容易地使用kotlin标准库中以lambda作为参数的扩展函数。但要注意java中必须要显式地传递一个接收者对象作为第一个参数。在java中函数或者lambda可以返回Unit,但因为在kotlin中Unit类型是有一个值的,所以要显式地返回它。一个返回void地lambda不能作为返回Unit的函数类型的实参,就像之前例子中的(String)->Unit
声明函数类型的参数的时候可以指定参数的默认值,利用默认值去完成函数的一些基本功能
代码中使用了append,它总是使用toString方法将对象转换为字符串,现在我们可以利用函数类型的参数并用一个lambda作为它的默认值来让调用者更轻松更多样地调用该函数
使用
加入函数类型参数后可以对其进行其他更多的操作
注意这是一个泛型函数:有一个类型参数T表示集合中的元素的类型,lambda transform会接收这个类型的参数
声明函数类型的默认值只需要把lambda作为值放在=
后面。另一种方法是声明一个参数为可空的函数类型。注意这里不能直接调用作为参数传递进来的函数:kotlin会因为检测到潜在的空指针异常而导致编译失败,一种可选的办法是显式检查null
还有一个更简单的版本,它利用了一个事实,函数类型是一个包含invoke方法的接口的具体实现。作为一个普通方法,invoke
可以通过安全调用语法被调用
利用invoke重写joinToString函数
当程序中的一段逻辑可能会因为程序的状态或者其他条件而产生变化,比如说,运输费用的计算依赖于选择的运输方式。可以定义一个函数用来选择恰当的逻辑变体并将它作为另一个函数返回
定义一个返回函数的函数
声明一个返回另一个函数的函数需要指定一个函数类型作为返回类型。上图中getShippingCostCalculator返回了一个函数,这个函数以Order作为参数并返回一个Double类型的值。要返回一个函数需要写一个return表达式,跟上一个lambda、一个成员引用或其他的函数类型的表达式
函数类型和lambda表达式一起组成了一个创建可重用代码的好工具
如要显示来自Windows机器的平均访问时间
为了避免重复我们可以把平台类型抽象为一个参数
如果你对多个平台的平均访问事件感兴趣,此时无法再用一个简单的参数来表示不同平台
使用高阶函数去除重复代码
函数类型可以帮助去除重复代码,如果你禁不住复制粘贴了一段代码,那么很可能这段重复代码是可以避免的。使用lambda不仅可以抽取重复的数据也可以抽取重复的行为。
一些设计模式可以用函数类型和lambda表达式进行简化。比如策略模式
在没有lambda表达式的情况下你要声明一个接口并为每一种可能的策略提供实现类。实现函数类型可以用一个通用的函数类型来描述策略,然后传递不同的lambda表达式作为不同的策略
我们知道lambda表达式会被正常地编译成匿名类,这表示每一次调用lambda表达式时一个额外的的类就会被创建,若表达式捕捉了某个变量,那么每次调用都会创建一个新的对象。这会带来运行时的额外开销,导致使用lambda比使用一个直接执行相同代码的函数效率更低
若用inline
修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用
当一个函数被声明为inline时,它的函数体是内联的,函数体会被直接替换到函数被调用的地方,而不是被正常调用
调用这个函数的语法跟java中使用syncehronized语句完全一样。区别在于java中的synchronized可以用于任何对象,而这个函数需要传入一个Lock实例,kotlin标准库中有一个可接受任何对象作为参数的synchronized函数版本。因为已经将其声明为inline,所以每次调用它所生成的代码跟java中的synchronized语句一样
注意lambda表达式和synchronized函数的实现都被内联了。由lambda生成的字节码成为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中
在调用内联函数的时候也可以传递函数类型的变量作为参数,在这种情况下lambda的代码在内联函数被调用点是不可用的,因此不会被内联。只有synchronized的函数体被内联了,lambda才会被正常调用。如果在两个不同的位置使用同一个内联函数,但用的是不同的lambda,那么内联函数会在每一个被调用的位置分别内联。内联函数的代码会被拷贝到使用它的两个不同位置,并把不同的lambda替换到其中
不是所有使用lambda的函数都可以被内联。当函数被内联时,作为参数的lamnbda表达式的函数体会被直接替换到最终生成的代码中。这会限制函数体中对应lambda参数的使用。如果lambda参数被调用,这样的代码能被容易地内联。但如果lambda参数在某个地方被保存起来,以便后面可以继续使用,lambda表达式的代码将不能被内联,因为必须要有一个包含这些代码的对象存在
一般来说,参数如果被直接调用或者作为参数传递给另外一个inline函数,它是可以被内联的
如果一个函数期望多个lambda参数,可以选择只内联其中一些参数。因为一个lambda可能会包含很多代码或者以不允许内敛的方式使用。接收这样的非内联lambda的参数可以用noinline修饰符标记
编译器完全支持内联跨模块的函数或者第三方库定义的函数,也可以在java中调用绝大部分内联函数,但这些函数不会被内联,而是被编译成普通函数调用
在kotlin中filter函数被声明为内联函数。这意味着filter函数以及传递给它的lambda的字节码会一起内联到filter被调用的地方
以下图为例
filter和map函数都被声明为inline
函数,所以它们不会产生额外的类或对象
如果有大量变量要处理,中间集合的运行开销不能忽视,这时可以在调用链后加上一个asSequence调用,用序列来代替集合。但用来处理序列的lambda没有被内联,每一个中间序列被表示成把lambda保存在其字段中的对象,而末端操作会导致由每一个中间序列调用组成的调用链执行。所以小集合可以用普通的集合操作处理
使用inline关键只能提高带有lambda参数的函数性能。对于普通的函数调用JVM已经提供了强大的内联支持。将带有lambda参数的函数内联,不仅节约了函数调用的开销,而且节约了为lambda创建匿名类,以及创建lambda实例对象的开销。若你要内联的函数很大,在这种情况下你应该将那些与lambda参数无关的的代码抽取到一个独立的非内联函数中
资源管理:先获取一个资源,完成一个操作,然后释放资源。这里的资源可以表示很多不同的东西:一个文件、一个锁等等。实现这个模式的标准做法是使用try/catch语句,资源在try代码块之前被获取,在finally代码块中被释放。之前的一个例子展示了synchronized函数,它将一个锁对象作为参数。kotlin标准库定义了另一个叫做withLock的函数,它提供了实现同样功能的更符合语言习惯的API:它是Lock接口的扩展函数
需要加锁的代码被抽取到一个独立的方法中
文件是另一种可以使用这种模式的常见资源类型,java7为这种模式引入try-with-resource语句。kotlin中没有等价的语法,因为通过使用一个带有函数类型参数的函数(接收lambda参数)可以无缝完成相同的事情,这个kotlin标准库中的函数叫作use
use
是一个扩展函数,被用来操作可关闭的资源,它接收一个lambda作为参数。这个方法调用lambda并且确保资源被关闭,无论lambda正常执行还是抛出异常。它是一个内联函数。