本文收录于 kotlin入门潜修专题系列,欢迎学习交流。
创作不易,如有转载,还请备注。
写在前面
博学之,审问之,慎思之,明辨之,笃行之。——与君共勉。
高阶方法和lambda表达式
在kotlin中,方法是一等公民。什么是一等公民?翻译成编程语言对应的意思就是:kotlin中的方法同一般的变量一样,可以作为方法参数、可以赋值给其他变量等等。
那么作为静态强类型限制语言的kotlin是怎么做到呢?那就是通过提供方法类型(function types)以及lambda表达式来达到目标的。本篇文章将阐述kotlin中的这些特性。
高阶方法
高阶方法是指,那些入参类型包含方法类型或者返回值是个方法类型的方法。
所以,理解高阶方法之前,必然要理解另外一个定义:方法类型(function types)。
什么是方法类型?其实方法类型和我们常见的数字类型、字符串类型等等是一样的,都是用作定义变量的类型。只不过这个类型看着并不像我们平常定义的其他类型那么容易理解。来看个例子:
fun sayHello(str: String, checkStr: (str: String) -> Boolean) {
if (checkStr(str)) {
println("pass...")
} else {
println("error...")
}
}
上面代码定义了一个方法sayHello,这个方法唯一特别的地方就是该方法的第二个参数类型:checkStr: (str: String) -> Boolean 。我们咋一看可能就懵掉了:这个是什么鬼类型,完全看不明白!其实,这个类型就是方法类型!
方法类型的定义如同普通方法的声明一样,可以有入参也可以有返回值,下面针对方法类型做几点阐述:
- 方法类型中的参数类型需要使用()括号包裹,如(A, B)-> C中的A、B都需要包裹在小括号中。当然如果没有参数类型可以省略参数类型,如 ()->C就表示该方法类型没有参数类型。
- 方法类型中->后面的类型表示该方法类型的返回类型,如(A, B)-> C中的C表示方法的返回类型就是C。如果该方法类型没有返回类型(即默认返回Unit),则必须显示指定其返回类型为Unit,也就是不能省略Unit。
- 方法类型可以有额外的接收类型(receive type,如果不明白可以回顾扩展方法那篇文章),如 A.(B) -> C,该方法类型表示可以通过A对象来调用参数为B类型且返回值我C类型的方法。
- Suspending方法也是方法类型,只不过比较特殊,这个会在kotlin协程相关的文章中进行阐述。
- 方法类型中的入参类型可以指定参数名字,如(x:Int, y:Int)-> Int,这在生成文档时很有用处,增加了文档的可读性。
- 方法类型中的->符号是右结合的,所以方法类型(Int) ->((Int) ->Int) 和(Int) -> (Int) -> Int表达的是同一个类型,但是和方法类型((Int) ->(Int)) ->Int是不同的。
- 方法类型可以使用typealias关键字定义别名,如:typealias checkStr = (str: String) -> Boolean
好了,总结了一大堆,只不过是在脑子里面留个大概的印象,如果只看一遍而没有实践,过不多久肯定会忘记的。现在先回到前面一个例子中,我们定义了一个方法类型的入参,那么如何使用呢?示例如下:
//main测试方法
fun main(args: Array<String>) {
val testStr1 = ""
val testStr2 = "test"
sayHello(testStr1, { str -> str.isNotEmpty() })//打印'error...'
sayHello(testStr2, { str -> str.isNotEmpty() })//打印'pass...'
}
上面代码的打印结果已经在语句后面注释出来了,主要关注方法类型是如何调用的。
同其他类型传参一样,调用含有方法类型的方法时,必须要传入一个方法类型的实例,那么该怎么实例化一个方法类型呢?实例化方法类型可以通过以下几种方法:
- 使用字面量函数代码块,主要有一下两种:
(1)lamada表达式。如本例中的{str -> str.isNotEmpt()}就是采用这种方法。
(2)匿名方法。如上面的例子中我们还可以这么调用:
sayHello(testStr2, fun(str): Boolean { return str.isNotEmpty()})
- 传入已定义过的可调用的方法引用,这一类包括成员方法、扩展方法、top-level方法甚至某些构造方法等,如上面例子我们可以直接传入String中的isNotEmpty方法来完成我们的需求(这里以为只判断是否为空,如果有其他复杂逻辑则不能这么用了,除非有已经定义好的完全匹配的相应方法),如下所示:
sayHello(testStr2, String::isNotEmpty)
- 传入实现方法类型接口的自定义类对象。这个是什么意思?看下示例就会明白(注意注释):
//自定义了一个CheckStr类型,实现了 (String) -> Boolean接口
//注意,这里就不能写成 (str:String) -> Boolean,实现方法类型接口时
//不能有命名参数,只能有类型
class CheckStr : (String) -> Boolean {
override fun invoke(str: String): Boolean = str.isNotEmpty()
}
//调用方法如下
sayHello(testStr2, CheckStr())
至此,上面的例子算是分析完了。
接下来看下如何通过方法类型的实例来调用方法,示例如下:
fun main(args: Array<String>) {
//定义两个测试字符串
val testStr1 = ""
val testStr2 = "test"
//方法类型实例1:checkStr,接收两个参数,返回Boolean值。
//实例化的内容是判断str1以及str2是否为空,如果都不为
//空则返回true,否则返回false
val checkStr: (String, String) -> Boolean = { str1, str2 -> str1.isNotEmpty() && str2.isNotEmpty() }
println(checkStr.invoke(testStr1, testStr2))//调用方式1
println(checkStr(testStr1, testStr2))//调用方式2
//方法类型实例2:checkStr2,注意这里将上面两个参数方法类型
//赋值给了包含有一个receiver、但只有一个入参类型的方法类型
val checkStr2: String.(String) -> Boolean = checkStr
println(testStr1.checkStr2(testStr2))//可以直接通过receiver对象调用
}
上面代码主要阐述一下几点(原理将会在下面文章中分析):
- 方法类型的实例有两种调用方式,一种是像普通方法调用一样传入对应的参数即可,如checkStr(testStr1, testStr2);另一种是通过实例的invoke方法来调用如checkStr.invoke(testStr1, testStr2)。
- 对于包含有receiver的方法类型,如果receiver的类型以及该方法类型的剩余参数类型和没有显示定义receiver的方法类的入参类型相匹配,则二者可以进行相互赋值。这个理解起来有点费劲,看下示例说明就会明白:如方法类型String.(String) -> Boolean就是显示包含receiver的方法类型,该receiver的方法类型是String,参数类型也是String,所以它就等同于方法类型(String, String) -> Boolean ,该方法类型同时接收两个String,而第一个String类型刚好和receiver类型相匹配,剩下的参数类型也相互匹配,故可以认为他们是相等的。
Lambda表达式
Lambda表达式一听就很神秘的样子,其实没什么,他和匿名方法都被成为“方法字面量”,他们所表达的场景就是,在没有显示定义方法的时候,我们可以通过这种方式生成一个具有同等形式、功能的方法。
还是回到文章开头提到的sayHello这个例子中:
//sayHello方法,第二个参数类型是方法类型
fun sayHello(str: String, checkStr: (str: String) -> Boolean) {
}
//我们使用中可以这么调用
sayHello("test", { str -> str.isNotEmpty() })
//{ str -> str.isNotEmpty() }就是一个lambda表达式,其作用
//相当于下面显示定义的checkStr方法
fun checkStr(str: String): Boolean {
return str.isNotEmpty()
}
代码中的注释已经阐述了lambda表达式的含义,lambda表达式可以大大简化代码的写法,也能减少不必要的方法定义,但与此同时带来的副作用就是是代码的可读性大大降低。
lambda表达式定义的语法如下所示:
//直接给sum赋值一个方法类型实例,等于后面就是标识的lambda表达式
val sum = { x: Int, y: Int -> x + y }
//也可以显示定义sum的类型为(Int,Int)->Int的方法类型
val sum: (Int, Int) -> Int = { x, y -> x + y }
lambda表达式都会被放在{}中,可以指定参数名称也可以省略。当lambda表达式作为方法的最后一个参数时,可以将其提取到方法外部进行实现,如调用上面的sayHello方法可以写成如下形式:
//直接将lambda表达式提取到了方法体的外部
sayHello("test") { str -> str.isNotEmpty() }
当lambda表达式是唯一的方法入参时,我们可以只保留{},如下所示:
//假如这里我们定义了只包含一个lambda表达式入参的sayHello方法
fun sayHello(checkStr: (String) -> Boolean) {
if (checkStr("test")) {
println("is not empty...")
}
}
//那么我们就可以使用及其简洁的调用方式
sayHello { str -> str.isNotEmpty() }
上面这种就是我们经常见到的lambda方法调用形式,刚接触的时候会一脸懵逼,理解之后就会发现,实际上这种形式只有方法入参有且只有一个lambda表达式类型的时候的一种简写,这也正是lambda表达式的一个吸引人之处,简化代码。
在kotlin中,对于只有一个lambda入参的方法,还有一个优化的地方,那就是可以使用it来代替实际入参,比如上面调用sayHello的方式还可以简写为如下代码:
//it就指代了传入的str对象
sayHello { it -> it.isNotEmpty() }
上面代码中,我们并没有显示的返回lambda的结果,那么lambda的返回机制是什么呢?照例说话:
//1. 这个是我们常用的调用方法,并没有太多关注返回值
sayHello { it -> it.isNotEmpty() }
//2. 其实我们可以在{}中做更多工作,然后再{}的最后返回我们想要的值
//即lambda会将最后一条语句作为其返回值
sayHello {
val result = it.isNotEmpty()
result
}
//3. 这里我们显示指定了返回值,其效果和1、2中一样
//这里使用了精确返回,即return@sayHello,为什么要这样?
//这是因为如果直接return的话就相当于return当前方法,
//而通过指定label,就表示要return的是当前的方法块。
sayHello {
val result = it.isNotEmpty()
return@sayHello result
}
上面代码已经注释的很详尽,这里不再阐述。
如果一个高阶方法接收多个lambda表达式,当我们不需要传递时可以传入下划线_,如下所示:
//我们定义了一个sayHello方法,参数是一个方法类型,
//该方法类型需要两个String类型的参数
fun sayHello(checkStr: (String, String) -> Boolean) {
}
//当我们调用sayHello的时候,如果我们不需要传入参数,
//则可以使用_代替,如下代码意思就是不需要用到第一个入参
sayHello { _, str -> str.isNotEmpty() }
匿名方法
阐述过lambda表达式后,继续阐述下匿名方法。相较于lambda表达式,匿名方法容易理解多了。
匿名方法同lambda方法同样都是“方法字面量”,都可以在不显示定义方法的时候提供具有同样形式、功能的实现。只不过匿名方法更加接近普通方法的定义,如下所示:
//匿名方法的形式如下所示,但并不能这么在文件中定义!
fun(x: Int, y: Int): Int = x + y
当然匿名方法并不能当而皇之的写在class中或者top-level中,而只能作为方法实参传入到方法中,如上面的sayHello可以改用匿名方法的调用形式:
//通过匿名方法的形式调用sayHello
sayHello(fun(str: String): Boolean {
return str.isNotEmpty()
})
//当方法体只有一条语句时,可以简化为下面语句进行调用
sayHello(fun(str: String) = str.isNotEmpty())
匿名方法和lambada表达式的不同,主要有两点:
- 匿名方法中的入参,必须要放到圆括号里面。而lambda可以简化。
- 匿名方法返回时,会返回到该方法的调用处,而lambda则会返回最近的方法调用处。
闭包
什么是闭包?很难用一句话来解释,可以理解为,当一个作用域A位于另一个作用域B中时,A可以访问到其外部作用域B的相关环境,A和B所构成的环境就可以称之为一个闭包。
kotlin中的lambda表达式和匿名方法都可以访问其闭包中的相关环境,这里的相关环境其实就是指一些变量等。如下所示:
//main为测试方法
fun main(args: Array<String>) {
//main方法中有个变量initVal
var initVal = 1
//示例1,我们可以在lambda表达式中访main作用域中的initVal
sayHello {
initVal++
it.isNotEmpty()
}
//示例2,我们也可以在匿名方法中访main作用域中的initVal
sayHello(fun(str: String): Boolean {
initVal++
return str.isNotEmpty()
})
}
至此,高阶方法和lambda表达式的用法已阐述完毕。