Kotlin开发笔记:函数式编程
什么是函数式编程
简单来说,我们之前接触到的编程的主流就是命令式编程,我们需要告诉计算机做什么和如何做。而函数式编程的意思就是我们只需要告诉计算机我们想做什么,计算机会帮助我们实现如何做。我们可以看看维基百科的介绍:
在函数式编程中,函数是头等对象即头等函数,这意味着一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。λ演算是这种范型最重要的基础,λ演算的函数可以接受函数作为输入参数和输出返回值。
实际上就是让我们使用封装的函数来进行编程而不需要每时每刻都关心细节的实现,只有我们想要的时候才关注细节。这种函数式编程的方式本质上是声明式的,它与命令式的编程各有优劣。不过在这个声明式编程兴起的时代,我们还是需要学习函数式编程的思想。
Lambda表达式
作为函数式编程中最重要的一个部分,我们需要了解什么是Lambda表达式。Lambda表达式是没有名称的函数,其返回类型是推断的。
通常我们的函数有四个部分:名称,返回类型,参数列表和主体,Lambda只保留函数最重要的部分–参数列表和主体,在Kotlin中一般是这样表示的:
{parameter list -> body}
Lambda被包含在{ }中。使用连字符箭头(->)把主体与参数列表分开,主体通常是单个语句或者表达式,当然也可以是多行。
书中给出了一个小提示:将lambda作为参数传递给函数时,除非它是最后一个参数。否则不要急于创建多行lambda。这将不利于我们阅读代码。
一个小例子🌰
这里我们可以先演示一个简单的例子,比如说我们想要实现一个判断数是否为质数的函数,我们就可以这样写:
fun isPrime(n:Int):Boolean = n > 1 && (2 until n).none{i:Int -> n % i == 0}
这段代码的难以理解的点应该就是2 until n后面跟着的none函数了,就如同名字一样,none函数时判断范围内里有没有符合none接收的lambda表达式条件的数,一旦有一个符合条件,那么就返回false,否则返回true。
在这段代码内部我们可以吧n视为一个标量,而把i看做是一个变量,这样便于我们理解。一旦2到n-1的范围内没有一个数可以将n整除,那么n就是质数。
除此之外,Kotlin的类型推断可以简化我们的写法,我们不必显式指定i的类型,简化后的写法:
fun isPrime(n:Int):Boolean = n > 1 && (2 until n).none{i -> n % i == 0}
隐式参数
如果传递给参数的lambda只接受一个参数,那么我们可以省略参数声明用一个it来代替:
fun isPrime(n:Int):Boolean = n > 1 && (2 until n).none{ n % it == 0}
这里的it代表的就是范围内的每一个数。对于只带有一个参数的短lambda,我们可以省略参数声明和箭头->,并将it用于变量名。但是缺点是我们就不能很快地分辨一个lambda是不是有参数。对于长的lambda表达式来说,我们应该避免这种情况。
接收lambda
上面的none函数就是一个可以接收lambda表达式的函数,那么我们要怎么写一个接收lambda表达式的函数呢?我们继续以一个例子来介绍:
fun walkTo(action: (Int) -> Unit,n:Int) {
(1..n).forEach{action(it)}
}
第一个参数就是一个lambda表达式,实际上也可以理解为接受一个函数。这个action参数接受一个参数列表为一个Int,无返回值的lambda表达式。对传入的lambda表达式进行调用也很简单,像函数一样使用它就好。
我们在主函数中调用一下:
fun main() {
walkTo({ println("$it is working")},5)
}
输出是正常的:
将lambda表达式放在最后一个参数
上面的示例中我们将lambda表达式放在了参数列表的第一个位置上,这是个不好的习惯,这将不利于我们阅读代码。并且,将lambda表达式将允许我们简化一下函数的调用,我们先修改方法:
fun walkTo(n:Int,action: (Int) -> Unit) =
(1..n).forEach{action(it)}
现在,我们调用函数应该是这样的:
walkTo(5,{println("$it is working")})
但是我们此时也可以这样调用:
walkTo(5){ println("$it is working")}
对于多个参数,只需要将lambda表达书放在最后即可:
fun main() {
walkTo(5,5){println("$it is working")}
}
fun walkTo(n:Int,x:Int,action: (Int) -> Unit) =
(1..n).forEach{action(it)}
返回lambda表达式的函数
在Kotlin中我们还可以返回lambda表达式,比如我们可以在一个列表中查找指定字符长度的字符串,我们就可以用find函数接受一个lambda表达式,但是每次都重新写lambda表达式显然是令人厌烦的,我们可以创建一个函数,这个函数就专门用来返回lambda表达式:
fun searcgLength(n:Int):(String) -> Boolean {
return {name:String -> name.length == n}
}
这样我们就可以在主函数中通过这个函数来调用:
fun main() {
val names = listOf("aa","bbb","cccc","ddddd")
println(names.find(searcgLength(4)))
}
不过需要说明的是返回的lambda表达式本质上是函数的签名。它并不会自动执行函数。比如:
fun main() {
{println("jack")}
}
中间花括号这一对就是一个lambda表达式,但是它并不会自动打印内容,这样写可能更清晰。
fun main() {
val hanshu = {println("jack")}
hanshu() // 或者hanshu.invoke()
}
这样才会打印出jack。
lambda和匿名函数
匿名函数和lambda表达式在本质上可能是一种东西,都是函数的签名,或者说是指向函数的起始地址(可以这么理解),和lambda表达式一样,匿名函数也可以用变量存储起来,比如说以下两种形式:
val fn1 = fun(name:String):Boolean{return name.length == 5}
val fn2 = {name:String -> name.length == 5}
这两种都可以传入我们之前写的find函数之中,在这里我们也可以看出Kotlin中匿名函数的写法,其实就是省略了函数名的函数定义写法。
闭包和词法作用域
这里首先需要介绍闭包的概念:
闭包(Closure)是一个编程概念,指的是一个包含了函数及其相关引用环境(变量、状态)的组合。换句话说,闭包是一个函数及其能够访问的其外部作用域中的变量集合。闭包允许函数捕获其声明时的上下文,并在稍后的时间内引用这些变量,即使函数是在其原始上下文之外执行的。
闭包通常用于实现函数式编程的特性,例如将函数作为参数传递、返回函数作为结果等。
如果一个lambda想要依赖外部条件,那么这类lambda就被称为闭包–这是因为它关闭了定义范围来绑定到非局部的属性和方法。 我们可以模拟闭包的情景:
fun main() {
val factor = 2
val doubleIt = {e:Int -> e * factor}
}
可以看到,lambda表达式中的factor变量是定义在lambda表达式之外的。在lambda表达式之内遇到factor变量,编译器就将在定义的范围内查找这个factor变量是在哪里定义的,这称之为词法作用域。比如这个示例中,lambda中的factor变量就被绑定到了前一行的val factor = 2上。
非局部和带标签的return
默认情况下,lambda表达式是不允许有return关键字的,这也是其和匿名函数的一个重要区别,即使匿名函数需要返回一个值,那么匿名函数必须要有return关键字。不过在某些情况下,lambda表达式也可以使用return。
借用书中的例子,先来创建一个invokeWith方法,接受一个Int参数和一个lambda表达式:
fun invokeWith(n:Int,action: (Int) -> Unit){
println("enter invoke with$n")
action(n)
println("left invoke with$n")
}
接下来我们再创建一个caller函数,在这个caller函数中调用invokeWith函数:
fun caller(){
(1..3).forEach { i ->
invokeWith(i){
println("enter for $it")
if(it == 2){
return
}
println("exit for $it")
}
}
println("end of caller")
}
但是编译器会报错,它不允许我们用return关键字立即返回,原因是kotlin编译器不清楚我们到底要从哪个lambda中退出,究竟是invokeWith后面的lambda还是foreach后面的lambda,还是退出整个caller函数。
带标签的return
Kotlin在默认情况下是不允许我们使用return的,不过有两种例外:带标签的return和非局部return。我们先来介绍带标签的return。这个和C中的jumpTo有点像,简而言之就是指定return跳出的lambda表达式:
fun invokeWith(n:Int,action: (Int) -> Unit){
println("enter invoke with$n")
action(n)
println("left invoke with$n")
}
fun caller(){
(1..3).forEach { i ->
invokeWith(i) here@{
println("enter for $it")
if(it == 2){
return@here
}
println("exit for $it")
}
}
println("end of caller")
}
这里我们将invokeWith后面的lambda块标记为here标签,并在后面的return中使用,这样return跳出时就会跳过整个invokeWith块,我们来看运行结果:
可以看到并没有打印exit for 2,说明跳过了这一段。这个here标签是我们自己定义的,实际上也可以直接使用函数名,比如:
fun caller(){
(1..3).forEach { i ->
invokeWith(i) {
println("enter for $it")
if(it == 2){
return@invokeWith
}
println("exit for $it")
}
}
println("end of caller")
}
这里需要说明的是编译器不允许使用带标签的return返回到任意的外部范围–你只能从当前所包含的lambda返回,比如这样返回就是不行的:
我们不能返回到外部的lambda中。
非局部return
前面提到了Kotlin中默认是不允许我们在lambda中使用return的,除了带标签的return之外,还有一种情况也可以允许我们使用return,那就是使用内联函数。如果一个内联函数接受一个lambda表达式,那么在这个lambda表达式中也可以使用return,我们修改之前的例子:
fun caller(){
(1..3).forEach { i ->
if (i == 2){return}
invokeWith(i) {
println("enter for $it")
if(it == 2){
return@invokeWith
}
println("exit for $it")
}
}
println("end of caller")
}
当遍历到i==2的情况时将直接调用return退出整个caller函数,查看输出:
这里可以在foreach的lambda之中调用return就是因为foreach是内联函数:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
用inline标记为内联的函数中接收的lambda表达式之中就可以使用return,这就称之为非局部return。让我们来总结一下return的行为:
- 默认情况下,在lambda中不允许使用return
- 可以使用带标签的return跳出当前的lambda表达式
- 只有当接收lambda的函数标记为内联的时候才可以使用非局部return来跳出整个函数
- 如果我们可以使用非局部return从lambda中退出时,请记住这是从整个正在定义函数的函数之中退出的