浅谈Kotlin语法篇之Lambda表达式彻底解析(六)

简述: 今天带来的Kotlin浅谈系列的第六弹, 一块儿来聊下Kotlin中的lambda表达式。lambda表达式应该都不陌生,在Java8中引入的一个很重要的特性,将开发者从原来繁琐的语法中解放出来,但是很遗憾的是只有Java8版本才能使用。而Kotlin则弥补了这一问题,Kotlin中的lambda表达式与Java混合编程能够支持Java8如下的版本。那咱们带着如下几个问题一块儿来看下Kotlin中lambda表达式。java

  • 一、为何要使用Kotlin的lambda表达式(why)?
  • 二、如何去使用Kotlin的lambda表达式(how)?
  • 三、Kotlin的lambda表达式通常用在哪(where)?
  • 四、Kotlin的lambda表达式的做用域变量和变量捕获
  • 五、Kotlin的lambda表达式的成员引用

1、为何要使用Kotlin的lambda表达式?

针对以上为何使用Kotlin中的lambda表达式的问题,我以为有三点主要的缘由。android

  • 一、Kotlin的lambda表达式以更加简洁易懂的语法实现功能,使开发者从原有冗余啰嗦的语法声明解放出来。可使用函数式编程中的过滤、映射、转换等操做符处理集合数据,从而使你的代码更加接近函数式编程的风格。
  • 二、Java8如下的版本不支持Lambda表达式,而Kotlin则兼容与Java8如下版本有很好互操做性,很是适合Java8如下版本与Kotlin混合开发的模式。解决了Java8如下版本不能使用lambda表达式瓶颈。
  • 三、在Java8版本中使用Lambda表达式是有些限制的,它不是真正意义上支持闭包,而Kotlin中lambda才是真正意义的支持闭包实现。(关于这个问题为何下面会有阐述)

2、Kotlin的lambda表达式基本语法

一、lambda表达式分类

在Kotlin实际上能够把Lambda表达式分为两个大类,一个是普通的lambda表达式,另外一个则是带接收者的lambda表达式(功能很强大,以后会有专门分析的博客)。这两种lambda在使用和使用场景也是有很大的不一样. 先看下如下两种lambda表达式的类型声明:编程

针对带接收者的Lambda表达式在Kotlin中标准库函数中也是很是常见的好比with,apply标准函数的声明。性能优化

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
复制代码

看到以上的lambda表达式的分类,你是否是想到以前的扩展函数了,有没有想起以前这张图?bash

是否是和咱们以前博客说普通函数和扩展函数相似。普通的Lambda表达式相似对应普通函数的声明,而带接收者的lambda表达式则相似对应扩展函数。扩展函数就是这种声明接收者类型,而后使用接收者对象调用直接相似成员函数调用,实际内部是经过这个接收者对象实例直接访问它的方法和属性。闭包

二、lambda基本语法

lambda的标准形式基本声明知足三个条件:app

含有实际参数jvm

含有函数体(尽管函数体为空,也得声明出来)ide

以上内部必须被包含在花括号内部函数式编程

以上是lambda表达式最标准的形式,可能这种标准形式在之后的开发中可能见到比较少,更可能是更加的简化形式,下面就是会介绍Lambda表达式简化规则

三、lambda语法简化转换

之后开发中咱们更多的是使用简化版本的lambda表达式,由于看到标准的lambda表达式形式仍是有些啰嗦,好比实参类型就能够省略,由于Kotlin这门语言支持根据上下文环境智能推导出类型,因此能够省略,摒弃啰嗦的语法,下面是lambda简化规则。

注意:语法简化是把双刃剑,简化当然不错,使用简单方便,可是不能滥用,也须要考虑到代码的可读性.上图中Lambda化简成的最简单形式用it这种,通常在多个Lambda嵌套的时候不建议使用,严重形成代码可读性,到最后估计连开发者都不知道it指代什么了。好比如下代码:

这是Kotlin库中的joinToString扩展函数,最后一个参数是一个接收一个集合元素类型T的参数返回一个CharSequence类型的lambda表达式。

//joinToString内部声明
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
    return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}


fun main(args: Array<String>) {
    val num = listOf(1, 2, 3)
    println(num.joinToString(separator = ",", prefix = "<", postfix = ">") {
        return@joinToString "index$it"
    })
}
复制代码

咱们能够看到joinToString的调用地方是使用了lambda表达式做为参数的简化形式,将它从圆括号中提出来了。这个确实给调用带来一点小疑惑,由于并无显示代表lambda表达式应用到哪里,因此不熟悉内部实现的开发者很难理解。对于这种问题,Kotlin实际上给咱们提供解决办法,也就是咱们以前博客提到过的命名参数。使用命名参数后的代码

//joinToString内部声明
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
    return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
fun main(args: Array<String>) {
    val num = listOf(1, 2, 3)
    println(num.joinToString(separator = ",", prefix = "<", postfix = ">", transform = { "index$it" }))
}
复制代码

四、lambda表达式的返回值

lambda表达式返回值老是返回函数体内部最后一行表达式的值

package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {

    val isOddNumber = { number: Int ->
        println("number is $number")
        number % 2 == 1
    }

    println(isOddNumber.invoke(100))
}
复制代码

将函数体内的两个表达式互换位置后

package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {

    val isOddNumber = { number: Int ->
        number % 2 == 1
        println("number is $number")
    }

    println(isOddNumber.invoke(100))
}
复制代码

经过上面例子能够看出lambda表达式是返回函数体内最后一行表达式的值,因为println函数没有返回值,因此默认打印出来的是Unit类型,那它内部原理是什么呢?其实是经过最后一行表达式返回值类型做为了invoke函数的返回值的类型,咱们能够对比上述两种写法的反编译成java的代码:

//互换位置以前的反编译代码
package com.mikyou.kotlin.lambda;

import kotlin.jvm.internal.Lambda;

@kotlin.Metadata(mv = {1, 1, 10}, bv = {1, 0, 2}, k = 3, d1 = {"\000\016\n\000\n\002\020\013\n\000\n\002\020\b\n\000\020\000\032\0020\0012\006\020\002\032\0020\003H\n¢\006\002\b\004"}, d2 = {"<anonymous>", "", "number", "", "invoke"})
final class LambdaReturnValueKt$main$isOddNumber$1 extends Lambda implements kotlin.jvm.functions.Function1<Integer, Boolean> {
    public final boolean invoke(int number) {//此时invoke函数返回值的类型是boolean,对应了Kotlin中的Boolean
        String str = "number is " + number;
        System.out.println(str);
        return number % 2 == 1;
    }

    public static final 1INSTANCE =new 1();

    LambdaReturnValueKt$main$isOddNumber$1() {
        super(1);
    }
}


复制代码
//互换位置以后的反编译代码
package com.mikyou.kotlin.lambda;

import kotlin.jvm.internal.Lambda;

@kotlin.Metadata(mv = {1, 1, 10}, bv = {1, 0, 2}, k = 3, d1 = {"\000\016\n\000\n\002\020\002\n\000\n\002\020\b\n\000\020\000\032\0020\0012\006\020\002\032\0020\003H\n¢\006\002\b\004"}, d2 = {"<anonymous>", "", "number", "", "invoke"})
final class LambdaReturnValueKt$main$isOddNumber$1 extends Lambda implements kotlin.jvm.functions.Function1<Integer, kotlin.Unit> {
    public final void invoke(int number) {//此时invoke函数返回值的类型是void,对应了Kotlin中的Unit
        if (number % 2 != 1) {
        }
        String str = "number is " + number;
        System.out.println(str);
    }

    public static final 1INSTANCE =new 1();

    LambdaReturnValueKt$main$isOddNumber$1() {
        super(1);
    }
}

复制代码

五、lambda表达式类型

Kotlin中提供了简洁的语法去定义函数的类型.

() -> Unit//表示无参数无返回值的Lambda表达式类型

(T) -> Unit//表示接收一个T类型参数,无返回值的Lambda表达式类型

(T) -> R//表示接收一个T类型参数,返回一个R类型值的Lambda表达式类型

(T, P) -> R//表示接收一个T类型和P类型的参数,返回一个R类型值的Lambda表达式类型

(T, (P,Q) -> S) -> R//表示接收一个T类型参数和一个接收P、Q类型两个参数并返回一个S类型的值的Lambda表达式类型参数,返回一个R类型值的Lambda表达式类型
复制代码

上面几种类型前面几种应该好理解,估计有点难度是最后一种,最后一种实际上已经属于高阶函数的范畴。不过这里说下我的看这种类型的一个方法有点像剥洋葱一层一层往内层拆分,就是由外往里看,而后作拆分,对于自己是一个Lambda表达式类型的,先暂时看作一个总体,这样就能够肯定最外层的Lambda类型,而后再用相似方法往内部拆分。

六、使用typealias关键字给Lambda类型命名

咱们试想一个场景就是可能会用到多个lambda表达式,可是这些lambda表达式的类型不少相同,咱们就很容易把全部相同一大串的Lambda类型重复声明或者你的lambda类型声明太长不利于阅读。实际上不须要,对于Kotlin这门反对一切啰嗦语法的语言来讲,它都给你提供一系列的解决办法,让你简化代码的同时又不下降代码的可读性。

fun main(args: Array<String>) {
    val oddNum:  (Int) -> Unit = {
        if (it % 2 == 1) {
            println(it)
        } else {
            println("is not a odd num")
        }
    }

    val evenNum:  (Int) -> Unit = {
        if (it % 2 == 0) {
            println(it)
        } else {
            println("is not a even num")
        }
    }

    oddNum.invoke(100)
    evenNum.invoke(100)
}
复制代码

使用typealias关键字声明(Int) -> Unit类型

package com.mikyou.kotlin.lambda

typealias NumPrint = (Int) -> Unit//注意:声明的位置在函数外部,package内部

fun main(args: Array<String>) {
    val oddNum: NumPrint = {
        if (it % 2 == 1) {
            println(it)
        } else {
            println("is not a odd num")
        }
    }

    val evenNum: NumPrint = {
        if (it % 2 == 0) {
            println(it)
        } else {
            println("is not a even num")
        }
    }

    oddNum.invoke(100)
    evenNum.invoke(100)
}
复制代码

3、Kotlin的lambda表达式常用的场景

  • 场景一: lambda表达式与集合一块儿使用,是最多见的场景,能够各类筛选、映射、变换操做符和对集合数据进行各类操做,很是灵活,相信使用过RxJava中的开发者已经体会到这种快感,没错Kotlin在语言层面,无需增长额外库,就给你提供了支持函数式编程API。
package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {
    val nameList = listOf("Kotlin", "Java", "Python", "JavaScript", "Scala", "C", "C++", "Go", "Swift")
    nameList.filter {
        it.startsWith("K")
    }.map {
        "$it is a very good language"
    }.forEach {
        println(it)
    }

}
复制代码

  • 场景二: 替代原有匿名内部类,可是须要注意一点就是只能替代含有单抽象方法的类。
findViewById(R.id.submit).setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				...
			}
		});
复制代码

用kotlin lambda实现

findViewById(R.id.submit).setOnClickListener{
    ...
}
复制代码
  • 场景三: 定义Kotlin扩展函数或者说须要把某个操做或函数当作值传入的某个函数的时候。
fun Context.showDialog(content: String = "", negativeText: String = "取消", positiveText: String = "肯定", isCancelable: Boolean = false, negativeAction: (() -> Unit)? = null, positiveAction: (() -> Unit)? = null) {
	AlertDialog.build(this)
			.setMessage(content)
			.setNegativeButton(negativeText) { _, _ ->
				negativeAction?.invoke()
			}
			.setPositiveButton(positiveText) { _, _ ->
				positiveAction?.invoke()
			}
			.setCancelable(isCancelable)
			.create()
			.show()
}

fun Context.toggleSpFalse(key: String, func: () -> Unit) {
	if (!getSpBoolean(key)) {
		saveSpBoolean(key, true)
		func()
	}
}

fun <T : Any> Observable<T>.subscribeKt(success: ((successData: T) -> Unit)? = null, failure: ((failureError: RespException?) -> Unit)? = null): Subscription? {
	return transformThread()
			.subscribe(object : SBRespHandler<T>() {
				override fun onSuccess(data: T) {
					success?.invoke(data)
				}

				override fun onFailure(e: RespException?) {
					failure?.invoke(e)
				}
			})
}

复制代码

4、Kotlin的lambda表达式的做用域中访问变量和变量捕获

一、Kotlin和Java内部类或lambda访问局部变量的区别

  • 在Java中在函数内部定义一个匿名内部类或者lambda,内部类访问的函数局部变量必须须要final修饰,也就意味着在内部类内部或者lambda表达式的内部是没法去修改函数局部变量的值。能够看一个很简单的Android事件点击的例子
public class DemoActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        final int count = 0;//须要使用final修饰
        findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println(count);//在匿名OnClickListener类内部访问count必需要是final修饰
            }
        });
    }
}

复制代码
  • 在Kotlin中在函数内部定义lambda或者内部类,既能够访问final修饰的变量,也能够访问非final修饰的变量,也就意味着在Lambda的内部是能够直接修改函数局部变量的值。以上例子Kotlin实现

访问final修饰的变量

class Demo2Activity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_demo2)
		val count = 0//声明final
		btn_click.setOnClickListener {
			println(count)//访问final修饰的变量这个是和Java是保持一致的。
		}
	}
}

复制代码

访问非final修饰的变量,并修改它的值

class Demo2Activity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_demo2)
		var count = 0//声明非final类型
		btn_click.setOnClickListener {
			println(count++)//直接访问和修改非final类型的变量
		}
	}
}

复制代码

经过以上对比会发现Kotlin中使用lambda会比Java中使用lambda更灵活,访问受到限制更少,这也就回答本博客最开始说的一句话,Kotlin中的lambda表达式是真正意义上的支持闭包,而Java中的lambda则不是。Kotlin中的lambda表达式是怎么作到这一点的呢?请接着看

二、Kotlin中lambda表达式的变量捕获及其原理

  • 什么是变量捕获?

经过上述例子,咱们知道在Kotlin中既能访问final的变量也能访问或修改非final的变量。原理是怎样的呢?在此以前先抛出一个高大上的概念叫作lambdab表达式的变量捕获。实际上就是lambda表达式在其函数体内能够访问外部的变量,咱们就称这些外部变量被lambda表达式给捕获了。有了这个概念咱们能够把上面的结论变得高大上一些:

第一在Java中lambda表达式只能捕获final修饰的变量

第二在Kotlin中lambda表达式既能捕获final修饰的变量也能访问和修改非final的变量

  • 变量捕获实现的原理

咱们都知道函数的局部变量生命周期是属于这个函数的,当函数执行完毕,局部变量也就是销毁了,可是若是这个局部变量被lambda捕获了,那么使用这个局部变量的代码将会被存储起来等待稍后再次执行,也就是被捕获的局部变量是能够延迟生命周期的,针对lambda表达式捕获final修饰的局部变量原理是局部变量的值和使用这个值的lambda代码会被一块儿存储起来;而针对于捕获非final修饰的局部变量原理是非final局部变量会被一个特殊包装器类包装起来,这样就能够经过包装器类实例去修改这个非final的变量,那么这个包装器类实例引用是final的会和lambda代码一块儿存储

以上第二条结论在Kotlin的语法层面来讲是正确的,可是从真正的原理上来讲是错误的,只不过是Kotlin在语法层面把这个屏蔽了而已,实质的原理lambda表达式仍是只能捕获final修饰变量,而为何kotlin却能作到修改非final的变量的值,实际上kotlin在语法层面作了一个桥接包装,它把所谓的非final的变量用一个Ref包装类包装起来,而后外部保留着Ref包装器的引用是final的,而后lambda会和这个final包装器的引用一块儿存储,随后在lambda内部修改变量的值其实是经过这个final的包装器引用去修改的。

最后经过查看Kotlin修改非final局部变量的反编译成的Java代码就是一目了然了

class Demo2Activity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_demo2)
		var count = 0//声明非final类型
		btn_click.setOnClickListener {
			println(count++)//直接访问和修改非final类型的变量
		}
	}
}

复制代码
@Metadata(
   mv = {1, 1, 9},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0014¨\u0006\u0007"},
   d2 = {"Lcom/shanbay/prettyui/prettyui/Demo2Activity;", "Landroid/support/v7/app/AppCompatActivity;", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "production sources for module app"}
)
public final class Demo2Activity extends AppCompatActivity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361820);
      final IntRef count = new IntRef();//IntRef特殊的包装器类的类型,final修饰的IntRef的count引用
      count.element = 0;//包装器内部的非final变量element
      ((Button)this._$_findCachedViewById(id.btn_click)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            int var2 = count.element++;//直接是经过IntRef的引用直接修改内部的非final变量的值,来达到语法层面的lambda直接修改非final局部变量的值
            System.out.println(var2);
         }
      }));
   }

   public View _$_findCachedViewById(int var1) {
      if(this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if(var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if(this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

复制代码

三、Kotlin中lambda表达式变量捕获注意事项

注意: 对于Lambda表达式内部修改局部变量的值,只会在这个Lambda表达式被执行的时候触发。

5、Kotlin的lambda表达式的成员引用

一、为何要使用成员引用

咱们知道在Lambda表达式能够直接把一个代码块做为一个参数传递给函数,可是有没有遇到过这样一个场景就是我要传递过去的代码块,已是做为了一个命名函数存在了,此时你还须要重复写一个代码块传递过去吗?确定不是,Kotlin拒绝啰嗦重复的代码。因此只须要成员引用替代便可。

fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy({ p: Person -> p.age }))
}
复制代码

能够替代为

fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy(Person::age))//成员引用的类型和maxBy传入的lambda表达式类型一致
}
复制代码

二、成员引用的基本语法

成员引用由类、双冒号、成员三个部分组成

三、成员引用的使用场景

  • 成员引用最多见的使用方式就是类名+双冒号+成员(属性或函数)
fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy(Person::age))//成员引用的类型和maxBy传入的lambda表达式类型一致
}
复制代码
  • 省略类名直接引用顶层函数(以前博客有专门分析)
package com.mikyou.kotlin.lambda

fun salute() = print("salute")

fun main(args: Array<String>) {
    run(::salute)
}
复制代码
  • 成员引用用于扩展函数
fun Person.isChild() = age < 18

fun main(args: Array<String>){
    val isChild = Person::isChild
    println(isChild)
}

复制代码

到这里有关Kotlin lambda的基础知识就基本浅谈完毕了,下一篇会从Lambda实质原理和字节码方面分析,以及Lambda表达式使用时性能优化。

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~

相关文章
相关标签/搜索