整齐是kotlin语法的一大特性,它主要体现在扩展函数,中缀调用,运算符重载,约好,以及对lambda表达式的活用,其实在用java写Android的时分咱们现已遇到过lambda了,比方当你在设置一个控件的点击监听事情的时分
咱们通常会这样写,然后就发现在点击事情的参数部分,代码变灰然后还有一条波浪线,提示说匿名类View.OnClickListener()能够被替换成lambda表达式,所以咱们就按照提示将这个点击事情转化成lambda
bindingView.button.setOnClickListener(v -> {});
很简洁的一行代码就生成了,其中v是参数,后边箭头紧跟着一个花括号,花括号里边便是你要写的逻辑代码,信任这个咱们都清楚,而在kotlin中,做了进一步简化,它能够将这个lambda表达式放在括号外面,并且能够将参数省掉
bindingView.button.setOnClickListener {}
代码更加简洁了,而lambda在kotlin中的表现远远不止这些,还能够将整个lambda作为一个函数的参数,典型的比方便是在运用规范库中的filter,map函数,或许Flow里边的操作符,举个比方,在一个姓名的调集中,咱们要对这个调集做一个过滤的操作,首字母为s的才干够被输出,代码如下
listOf("shifang","zhaoerzhu","sundashen").filter { it.startsWith("s") }
在这个比方中filter函数便是接收了一个lambda参数,咱们将整个lambda表达式显示出来便是这样
listOf("shifang","zhaoerzhu","sundashen").filter { it -> it.startsWith("s") }
所以在kotlin中,将类似于filter这样能够承受lambda或许函数引证作为参数的函数,或许回来值是lambda或许函数引证的函数,称之为高阶函数,这篇文章,会从以下几点渐渐介绍高阶函数
- 什么是函数类型
- 怎样去调用一个高阶函数
- 给函数类型设置默许值
- 回来值为函数类型的高阶函数
- 内联函数
- inline,noinline和crossinline润饰符
- 在lambda中运用return
函数类型
咱们刚开端学敲代码的时分,根本都是从数据类型开端学的,什么整数类型,浮点数类型,布尔值类型,都很熟悉了现已,到了kotlin这边,又多出来了一个函数类型,这是啥?咱们刚刚说到filter是高阶函数,而入参是函数的才干被叫做是高阶函数,所以咱们看看filter这个函数里边长什么样子的
咱们看到filter的参数部分,predicate是变量,而冒号后边便是跟的参数类型了,咱们总算看到函数类型长啥样了,一个括号,里边跟一个泛型T,其实也便是函数的参数类型,后边一个箭头,箭头后边跟着回来值类型,所以咱们声明一个函数类型的变量能够这样做
val findName : (String) -> Boolean
val sum : (Int,Int) -> Int
括号里边便是函数的参数类型跟参数数量,箭头后边是函数的回来值类型,这个时分咱们在想一个问题,既然是函数类型,那肯定承受的便是一个函数,咱们知道在kotlin中一个函数如果什么也不必回来,那么这个函数的回来值能够用Unit的来表明
fun showMessage():Unit {
println()
}
但通常咱们都是省掉Unit
fun showMessage() {
println()
}
那是不是函数类型里边,回来值如果是Unit,咱们也能够省掉呢?这样是不行的,函数类型中就算这个函数什么都不回来,咱们也要显示的将回来类型Unit表明出来,同样的,如果函数没有参数,也要指定一个空的括号,表明这个函数无参
val showMessage:() -> Unit
到了这儿,咱们就现已清楚了为什么在lambda表达式里{x,y -> x+y},或许最初那个比方,filter函数中{ it -> it.startsWith(“s”),变量的类型都省掉了,那便是由于这些变量类型现已在函数类型的声明中被指定了
当然函数类型也是能够为空的,同其他数据类型相同,当你要声明一个可空的函数类型的时分,咱们能够这样做
val sum : (Int,Int) -> Int?
上述代码其实犯了一个错误,它并不能表明一个可空的函数类型,它只能表明这个函数的回来值能够为空,那怎样表明一个可空的函数类型呢?咱们应该在整个函数类型外面加一个括号,然后在括号后边指定它是能够为空的,像这样
val sum : ((Int,Int) -> Int)?
调用高阶函数
知道了函数类型今后,咱们就要开端去手写高阶函数了,比方现在有一个需求,要求修正框内输入的内容里边只能包括字母以及空格,其他的都要过滤掉,那咱们就给String增加一个扩展函数吧,这个函数承受一个函数类型的变量,这个函数类型的参数是一个字符,回来类型是一个布尔值,表明契合条件的字符才干够被输出,咱们看下这个函数怎样完成
fun String.findLetter(judge:(Char) -> Boolean):String{
val mBuilder = StringBuilder()
for(index in indices){
if(judge(get(index))){
mBuilder.append(get(index))
}
}
return mBuilder.toString()
}
内部完成便是这样,对输入的字符串逐一字符进行遍历,经过调用judge函数来判断每个字符,契合条件的就输出,不契合的就过滤掉,高阶函数有了,咱们现在去调用它
println("what 8is4 ko4tli3n".findLetter { it in 'a'..'z' || it == ' ' })
整个花括号里边便是一个函数,它作为一个参数传递给findLetter,咱们看下运转成果
I what is kotlin
彻底按照条件输出,这样做的好处便是,如果下次需求变了,要求空格也不能输出,那么咱们彻底不需求去更改findLetter的代码,只需求更改一下作为函数类型的函数就能够了,就像这样
println("what 8is4 ko4tli3n".findLetter { it in 'a'..'z' })
运转成果就变成了
I whatiskotlin
咱们再换个比方,刚刚是给String界说了一个类似于过滤效果的函数,现在去界说一个映射效果函数,比方给输入的内容每个字符之间都用逗号隔开,咱们该怎样做呢
fun String.turn(addSplit: (Char) -> String): String {
val mBuilder = StringBuilder()
for (index in indices){
if(index != indices.last){
mBuilder.append(addSplit(get(index)))
}else{
mBuilder.append(get(index))
}
}
return mBuilder.toString()
}
代码与findLetter相似,略微做了一点改变,咱们看到这个高阶函数的入参类型变成了(Char)->String,表明输入一个字符,回来的是一个字符串,函数类型addSplit在这儿就充当着一个字符串的角色,咱们看下怎样去调用这个高阶函数
println("abcdefg".turn { "${it}," })
咱们看见turn后边的花括号里边就一个字符串,这个字符串是每个字符后边追加一个逗号,咱们看下运转成果
I a,b,c,d,e,f,g
函数类型的默许值
关于映射函数turn,咱们再改下需求,某些场景下,咱们输入什么就期望输出什么,比方用户设置昵称,根本是没有任何条件约束的,咱们改造下turn函数,让它能够接收空的函数类型,那这个咱们在刚刚函数类型那部分讲过,只需求在整个函数类型外面加个括号,然后加上可空标识就好了,改造完之后turn函数就变成了这样
fun String.turn(addSplit: ((Char) -> String)?): String {
val mBuilder = StringBuilder()
for (index in indices){
if(index != indices.last){
mBuilder.append(addSplit?.let { it(get(index)) })
}else{
mBuilder.append(get(index))
}
}
return mBuilder.toString()
}
看起来没什么问题,可是当你去调用这个turn函数,不传入任何函数类型参数的时分,咱们发现代码提示报错了
理由是addSplit这个参数必定要有个值,也便是说有必要得传点啥吗?也不必定,咱们知道kotlin函数中,参数是能够设置默许值的,那么函数类型的参数当然也能够设置默许值,就算什么也不传,它默许有一种完成办法,这样不就好了吗,咱们再改下turn函数
fun String.turn(addSplit: ((Char) -> String)? = { it.toString() }): String {
val mBuilder = StringBuilder()
for (index in indices) {
if (index != indices.last) {
mBuilder.append(addSplit?.let { it(get(index)) })
} else {
mBuilder.append(get(index))
}
}
return mBuilder.toString()
}
这样就不报错了,默许输入啥就输出啥,咱们看下运转成果
I abcdefg
函数类型作为回来值
刚刚咱们举的比方是作为入参的函数类型,现在咱们看下作为回来值的函数类型,这个其实咱们平时开发傍边也常常遇到,比方在一段代码中由于某个或许某几个条件,决定的不是一个值,而是会走到不同的逻辑代码中,这个时分咱们脑补下如果这些代码都写在一同那是不是一个函数就显的比较臃肿了,可读性也变差了,所以咱们就像return某一个值相同,将一段逻辑代码也return出去,这样代码逻辑就显的清晰许多,咱们新增一个combine函数,回来值是函数类型
fun String.combine(): (String) -> String {
val mBuilder = StringBuilder()
return {
mBuilder.append(it)
for (index in indices) {
if (index != indices.last) {
mBuilder.append("${get(index)},")
} else {
mBuilder.append(get(index))
}
}
mBuilder.toString()
}
}
combine不接收任何参数了,回来值变成了(String) -> String,咱们现在尝试着调用combine函数看看会输出什么呢
println("abcdefg".combine())
I Function1<java.lang.String, java.lang.String>
咱们看到并没有输出期望的成果,这个是为什么呢?我再回到代码中看看
咱们发现在回来值代码的边上,标明的这个回来值是一个lambda,并不是一个String,这个也便是函数类型作为回来值形成的成果,回来的是一个函数,函数你不去履行它,怎样可能会有成果呢,所以履行这个函数的办法便是调用invoke
println("abcdefg".combine().invoke("转化字符串:"))
invoke办法咱们仍是很熟悉的,在java里边去反射某一个类里边的办法的时分,终究去履行这个method便是用的invoke,而kotlin里边的invoke其实仍是一个约好,当lambda要去调用invoke函数去履行lambda自身的函数体时,invoke能够省掉,直接在lambda函数体后边加()以及参数,至于约好这儿就不打开说了,我会另起一篇文章单独讲,所以上面的代码咱们还能够这样写
println("abcdefg".combine()("转化字符串:"))
两种写法的运转成果都相同的,成果都是
I 转化字符串:a,b,c,d,e,f,g
内联函数
lambda带来的功用开销
咱们刚刚看到一个lambda的函数需求调用invoke办法才干够履行,那么这个invoke办法从哪里来的呢?凭什么调用它这个函数就能够履行了呢?咱们将之前写的代码转化成java找找原因
public static final Function1 combine(@NotNull final String $this$combine) {
final StringBuilder mBuilder = new StringBuilder();
return (Function1)(new Function1() {
public Object invoke(Object var1) {
return this.invoke((String)var1);
}
@NotNull
public final String invoke(@NotNull String it) {
Intrinsics.checkNotNullParameter(it, "it");
mBuilder.append(it);
int index = 0;
for(int var3 = ((CharSequence)$this$combine).length(); index < var3; ++index) {
if (index != StringsKt.getIndices((CharSequence)$this$combine).getLast()) {
mBuilder.append("" + $this$combine.charAt(index) + ',');
} else {
mBuilder.append($this$combine.charAt(index));
}
}
String var10000 = mBuilder.toString();
return var10000;
}
});
}
经过反编译咱们看到,本来这个lambda表达式便是界说了一个回调办法是invoke的匿名类Function1,Function后边跟着的1其实便是参数个数,咱们点到Function1里边看看
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
现在咱们知道刚刚没有调用invoke办法的时分,为什么会输出那一段信息了,其实那个便是把整个接口名称输出打印出来,只要调用了invoke这个回调办法,才会真实的去履行逻辑代码,把真实的成果输出,与此一起,咱们注意到在反编译代码中,每一次调用turn函数,都会生成一个Function1的目标,如果被屡次调用的话,很简单会形成必定的功用损耗,针对这种情况,咱们应该怎样去防止呢
inline
针对lamnda带来的功用开销,kotlin里边会运用inline润饰符去解决,用法也很简单,只要在高阶函数的最前面用inline去润饰就好了,咱们新增一个inlineturn函数,与turn函数相似,仅仅用inline去润饰
fun String.turn(addSplit: (Char) -> String): String {
val mBuilder = StringBuilder()
for (index in indices) {
if (index != indices.last) {
mBuilder.append(addSplit(get(index)))
} else {
mBuilder.append(get(index))
}
}
return mBuilder.toString()
}
inline fun String.inlineturn(addSplit: (Char) -> String): String {
val mBuilder = StringBuilder()
for (index in indices) {
if (index != indices.last) {
mBuilder.append(addSplit(get(index)))
} else {
mBuilder.append(get(index))
}
}
return mBuilder.toString()
}
两个函数的代码根本相似,仅仅inlineturn函数是用inline润饰的,在kotlin里边,对这种用inline润饰的高阶函数称之为内联函数,咱们去调用下这两个函数,然后反编译看看有什么差异吧
kotlin代码
println("abcdefg".turn { "${it}," })
println("abcdefg".inlineturn { "${it}," })
反编译后的java代码
String $this$inlineturn$iv = StringKt.turn("abcdefg", (Function1)null.INSTANCE);
System.out.println($this$inlineturn$iv);
$this$inlineturn$iv = "abcdefg";
int $i$f$inlineturn = false;
StringBuilder mBuilder$iv = new StringBuilder();
int index$iv = 0;
for(int var6 = ((CharSequence)$this$inlineturn$iv).length(); index$iv < var6; ++index$iv) {
if (index$iv != StringsKt.getIndices((CharSequence)$this$inlineturn$iv).getLast()) {
char it = $this$inlineturn$iv.charAt(index$iv);
int var8 = false;
String var10 = "" + it + ',';
mBuilder$iv.append(var10);
} else {
mBuilder$iv.append($this$inlineturn$iv.charAt(index$iv));
}
}
String var10000 = mBuilder$iv.toString();
$this$inlineturn$iv = var10000;
System.out.println($this$inlineturn$iv);
咱们看到turn办法不出所料,每次调用都会生成一个Function1的目标,而inlineturn函数反编译后咱们发现,这不便是将invoke办法里边的代码仿制出来放到外面来履行吗,所以现在咱们知道内联函数的工作原理了,便是将函数体仿制到调用途去履行,而此时,内联函数inlineturn的函数类型参数addSplit就不再是一个目标,而仅仅一个函数体了
noinline和crossinline
咱们现在现已有了一个概念了,inline润饰符什么时分适合运用
- 当函数是一个高阶函数
- 由于编译器需求将内联函数体代码仿制到调用途,所以函数体代码量比较小的时分适合用inline润饰
但有些场景下,即使函数是高阶函数,也是不推荐运用inline润饰符的,比方说你的函数类型参数需求当作目标传给其他普通函数
inline fun String.inlineturn(addSplit: (Char)->String): String {
val mBuilder = StringBuilder()
for (index in indices) {
if (index != indices.last) {
mBuilder.append(addSplit(get(index)))
} else {
mBuilder.append(get(index))
}
}
turnAnother(addSplit)//这一行编译报错
return mBuilder.toString()
}
还有一种场景便是当你的函数类型参数是可空的
inline fun String.inlineturn(addSplit: ((Char)->String)?): String {//参数部分编译报错
val mBuilder = StringBuilder()
for (index in indices) {
if (index != indices.last) {
mBuilder.append(addSplit?.let { it(get(index)) })
} else {
mBuilder.append(get(index))
}
}
return mBuilder.toString()
}
这两段代码都会编译报错,而报错的信息也根本一致,信息傍边都会有这一句提示
Add ‘noinline’ modifier to the parameter declaration
到了这儿咱们遇到了一个新的润饰符noinline,从字面意思上并联系上下文,咱们知道了这个noinline的效果,便是在内联函数中,运用noinline润饰的函数类型参数能够不参加内联,它依然是一个目标,反编译的时分它依然会被转成一个匿名类,尽管它是在一个内联函数中。 咱们运用noinline润饰符更改一下inlineturn函数,然后再反编译看看java代码中的差异
String $this$inlineturn$iv = StringKt.turn("abcdefg", (Function1)null.INSTANCE);
System.out.println($this$inlineturn$iv);
$this$inlineturn$iv = "abcdefg";
Function1 addSplit$iv = (Function1)null.INSTANCE;
int $i$f$inlineturn = false;
StringBuilder mBuilder$iv = new StringBuilder();
int index$iv = 0;
for(int var7 = ((CharSequence)$this$inlineturn$iv).length(); index$iv < var7; ++index$iv) {
if (index$iv != StringsKt.getIndices((CharSequence)$this$inlineturn$iv).getLast()) { mBuilder$iv.append((String)addSplit$iv.invoke($this$inlineturn$iv.charAt(index$iv)));
} else {
mBuilder$iv.append($this$inlineturn$iv.charAt(index$iv));
}
}
StringKt.turnAnother(addSplit$iv);
String var10000 = mBuilder$iv.toString();
$this$inlineturn$iv = var10000;
System.out.println($this$inlineturn$iv);
咱们看到原本是将函数体仿制出来的当地,现在变成了生成一个Function1的目标了,说明addSplit目标现已不参加内联了,而这个时分咱们注意到了,inlineturn函数前面的inline润饰符有了一个正告,提示说这个润饰符现已不需求了,建议去掉
关于这种正告我觉得仍是不能去疏忽的,由于咱们现已在反编译的代码中看到了,尽管addSplit不参加内联,但仍是会将函数体的代码仿制出来,关于编译器来讲仍是会有损耗的,所以这种情况下仍是把inline和noinline润饰符去掉,让它变成一个普通的高阶函数
现在咱们再换个场景,有时分一个函数类型的目标它履行起来比较耗时,咱们不能让它在主线程运转,那就有必要在将这个目标套在一个线程里边运转
inline fun String.inlineturn(addSplit: (Char)->String): String {
val mBuilder = StringBuilder()
Runnable{
for (index in indices) {
if (index != indices.last) {
mBuilder.append(addSplit(get(index)))//addSplit这边编译报错
} else {
mBuilder.append(get(index))
}
}
}
return mBuilder.toString()
}
咱们发现这边又编译报错了,内联函数怎样回事啊?事儿这么多。。。咱们看下这次报错提示是什么
Can’t inline ‘addSplit’ here: it may contain non-local returns. Add ‘crossinline’ modifier to parameter declaration ‘addSplit’
意思是不能对addSplit进行内联,原因是调用函数类型addSplit的当地与内联函数inlineturn归于不同的域,或许在inlineturn里边调用addSplit归于直接调用,所以在kotlin里边,如果内联函数中调用的函数类型,与内联函数自身归于直接调用的联系,那么函数类型前面需求加上crossinline润饰符,表明加强内联联系,咱们修正一下inlineturn函数,给addSplit加上 crossinline润饰符,代码就变成了
inline fun String.inlineturn(crossinline addSplit: ((Char)->String)): String {
val mBuilder = StringBuilder()
Runnable {
for (index in indices) {
if (index != indices.last) {
mBuilder.append(addSplit(get(index)))
} else {
mBuilder.append(get(index))
}
}
}
return mBuilder.toString()
}
学到这儿我信任不少人现已对高阶函数有了一个比较清晰的了解了,其实咱们在学习Flow的时分现已接触过这些高阶函数和内联函数了,比方咱们看下map操作符里边
map便是一个内联函数,而它里边的transform参数便是一个被crossinline润饰的函数类型的挂起函数,由于map里边的函数体必需求运转在一个协程域里边,而map又是运转在另一个协程域里边,map与transform之间归于直接调用的联系,这才用crossinline润饰
在lambda中运用return
现在给String再增加一个扩展函数,功用很简单,遍历String里边的每个字符,然后将字符在lambda的参数里边打印出来,一起要求如果遍历到字母,那么就停止打印。
fun String.filterAndPrint(filter:(Char) -> Unit){
for (index in indices) {
filter(get(index))
}
}
private fun test() {
"153a667".filterAndPrint {
if(it in 'a'..'z'){
return
}
println(it)
}
println("outside of foreach")
}
代码大概便是这样去完成,可是咱们发现写完代码后编译器在return的那个当地报错了,提示说这儿不允许运用return
‘return’ is not allowed here
这个是什么原因呢,kotlin官方文档中有这么一段描绘
要退出一个 lambda 表达式,咱们有必要运用一个标签,并且在 lambda 表达式内部制止运用裸
return
,由于 lambda 表达式不能使包括它的函数回来
kotlin为什么要这么设计呢?咱们结合上面讲到的内联函数就清楚了,由于当咱们在filterAndPrint函数里边return,退出的函数彻底取决于它是不是内联函数,如果是,咱们知道编译器会讲函数仿制到外面调用途的位置,那么return的便是test函数,而如果不是内联,那么退出的便是filterAndPrint自身,所以关于这么一种可能会导致抵触的作法,kotlin就约束了在普通lambda表达式里边不能运用return,如果必定要用,必需加上标签,也便是在return后边加上@以及lambda地点的函数名,咱们更改一下上面的test函数
private fun test() {
"153a667".filterAndPrint {
if(it in 'a'..'z'){
return@filterAndPrint
}
println(it)
}
println("outside of foreach")
}
加上标签今后编译器不报错了,咱们看下运转成果
I 1
I 5
I 3
I 6
I 6
I 7
I outside of foreach
咱们看到reutrn@filterAndPrint的时分并没有跳出test函数,仅仅跳过了a,持续循环打印后边的字符,这个就很想java里边continue的作法,但咱们的需求不是这样描绘的,咱们期望遇到字母今后就不打印后边的字符了,也便是直接跳出test函数,没错,便是将filterAndPrint变成内联函数就好了
inline fun String.filterAndPrint(filter:(Char) -> Unit){
for (index in indices) {
filter(get(index))
}
}
private fun test() {
"153a667".filterAndPrint {
if(it in 'a'..'z'){
return
}
println(it)
}
println("outside of foreach")
}
当lambda地点函数是内联函数的时分,lambda内部是能够return的,并且能够不必加标签,这个时分退出的函数便是调用内联函数地点的函数,也便是比方中的test(),咱们把这种回来称为非部分回来,咱们看下现在的运转成果
I 1
I 5
I 3
现在这个才是咱们想要的成果,现在回想一下最初刚开端学kotlin的时分,对没有break和continue关键字还有点不习惯,现在知道kotlin把这俩关键字去掉的原因了,由于彻底不需求,一个return加上内联函数就够了,想在哪个当地退出循环就在哪个当地退出。
总结
这篇文章咱们逐渐从函数类型开端,渐渐的认识了高阶函数,会去写高阶函数,也把握了inline,noinline,crossinline这些润饰符的效果以及运用场景,如果说之前你对高阶函数还很陌生的话,那么经过这篇文章,应该会对它熟悉一点了