Kotlin入门系列:第四章 Lambda编程

1 Lambda表达式

1.1 高阶函数

如果你有一个 a() 需要调用另外一个方法 b(),只需要在里面调用就可以:

int a() {
	// 在a方法调用b方法
	return b(1) + 1;
}

int b(int num) {
	return 1;
}

如果想在 a() 动态的调用 b() 传入参数,你需要在 a() 传入参数然后传给 b()

int a(int param) {
	// b()的参数由a()传入
	return b(param) + 1;
}

int b(int num) {
	return 1;
}

如果我想动态设置的不是方法的参数,而是方法本身呢?比如在 a() 可以传入一个方法,具体是什么方法我不知道,我只知道这里可以用传入的方法调用返回一个值:

// 传入的方法未知,可能是b(),可能是c()
int a(??? method) {
	return method() + 1;
}

int b(int num) {
	return 1;
}

int c(int num) {
	return 2;
}

在java中是不允许传入将方法作为参数传给另一个方法,但可以将这个方法用一个对象(接口)包裹起来传入,把这个接口类型作为外部方法的参数类型传入,用这个对象调用外部传入的方法:

public interface Wrapper {
	int method();
}

// 传入一个接口类型Wrapper对象,通过这个对象调用方法
int a(Wrapper wrapper) {
	return wrapper.method() + 1;
}

上面的说明如果觉得听晕了,我们换个写法:

// 本质上OnClickListener这个对象就是一个壳,实际我们需要的是OnClickListener的onClick()调用
OnClickListener listener = new OnClickListener() {
	@Override
	public void onClick(View v) {
		doSomething();
	}	
};
view.setOnClickListener(listener);

在kotlin中是可以将函数作为参数传递给另一个函数的,但需要你传入的是一个函数类型的对象给它:

// 将一个函数类型的对象传递给另一个函数
// 这个函数类型的对象名称为funParam,需要传入参数Int,最终返回String
// 作为函数的创建者,想要函数调用者给你传递一个函数类型的参数给你,只能这样写
fun a(funParam: (Int) -> String): String {
	return funParam(1)
}

fun b(param: Int): String {
	return param.toString()
}

函数除了可以作为参数传给另一个函数,还可以作为一个函数的返回值类型:

// 函数(Int) -> Unit作为返回值类型返回
fun c(param: Int): (Int) -> Unit {
	...
}

这种参数或者返回值为函数类型的函数被称为高阶函数。

1.2 函数引用

函数除了作为参数或者返回值的类型,把它赋值给一个变量也是可以的,但是需要加上 :: 双冒号才可以调用:

fun a(funParam: (Int) -> String): String {
	return funParam(1)
}

fun b(funParam: Int): String {
	return param.toString()
}

a(::b)
val d = ::b

这种写法被称为函数引用。

那为什么要加上 :: 双冒号才能使用,不能直接传递函数吗?因为只有加上双冒号,这个函数才变成了一个对象

在kotlin,一个函数可以作为参数,本质上是这个函数可以作为对象存在。只有对象才可以作为参数传递,也只有对象才能被赋值给变量;而kotlin的函数的本身性值又决定了它没办法被当作一个对象。

那怎么办?kotlin的选择是创建一个和函数具有相同功能的对象,创建这种对象就是在函数左边加上 :: 双冒号

函数左边加上 :: 双冒号已经不代表函数本身,而是表示它是一个对象,这个对象和函数具有相同的功能。

a(::b)
val d = ::b

// 调用函数
b(1) 

// 对象 d 后面加上括号来实现 b() 的等价操作
// d(1)是kotlin的语法糖,因为对象 d 是函数类型的对象,实际上调用的是d.invoke(1)
// 只有函数类型的对象可以使用invoke()
d(1) 

// 对象 :b 后面加上括号来实现 b() 的等价操作
// (::b)(1)是kotlin的语法糖,实际上调用的是(::b).invoke(1)
// 只有函数类型的对象可以使用invoke()
(::b)(1) 

函数名前面加上 :: 双冒号,刚才说了它是一个对象,其实也可以说它是一个指向对象的引用,但并不是指向函数本身,它指向了一个我们看不见的对象,这个对象它复制了原函数的功能,但它并不是原函数

fun b(param: Int): String {
	return param.toString()
}

val d = ::b
// 对象 d 是函数类型的对象,那对象 d 赋值给变量 e,是需要加 :: 还是不加?
// 这是一个赋值操作,赋值操作的右边是一个对象,那肯定是可以的
val e = d

1.3 匿名函数

函数除了作为参数传递和作为变量赋值,也可以直接挪过来使用:

fun a(funParam: (Int) -> String): String {
	return funParam(1)
}

fun b(param: Int): String {
	return param.toString()
}

// 直接将函数挪过来作为参数传递(实际是不允许这样写的在kotlin会报错,下面会讲到)
a(fun b(param: Int): String {
	return param.toString()
})

// 直接将函数赋值给变量(实际是不允许这样写的在kotlin会报错,下面会讲到)
val d = fun b(param: Int): String {
	return param.toString()
}

函数直接作为参数传递给另一个函数,或者直接赋值给变量,这种写法也可以将函数名省略,这就是匿名函数:

a(fun(param: Int): String {
	return param.toString()
})
val d = fun(param: Int): String {
	return param.toString()
}

刚才上面说到直接将带有函数名的函数作为参数传递给另一个函数或直接赋值给变量,这种写法在kotlin是不允许的,右边的函数既然要名字也没用,kotlin干脆就让你不用名字了。

1.4 Lambda表达式语法

如果在java中设计回调和使用是这样的:

public interface OnClickListener {
	void onClick(View v);
}

public void setOnClickListener(OnClickListener listener) {
	this.listener = listener;
}

view.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		// do something...
	}
});

那么在kotlin中就可以这样使用:

fun setOnClickListener(onClick: (View) -> Unit) {
	this.onClick = onClick
}

view.setOnClickListener(fun(v: View): Unit {
	// do something...
})

另外大多数情况下,匿名函数还可以再简化一点:

view.setOnClickListener({ v: View ->
	// do something...	
})

终于可以讲到Lambda表达式,如果是函数调用的最后一个实参,可以把表达式放到外面:

view.setOnClickListener() { v: View ->
	// do something...
}

当lambda是函数唯一的实参时,还可以去掉调用代码中的空括号:

view.setOnClickListener { v: View ->
	// do something...
}

如果lambda是单参数的,它的参数又可以省略,默认会生成一个名为 it 的参数(当有多个参数时,还是建议写清楚各个参数名):

view.setOnClickListener {
	it.setVisibility(View.GONE)
}

lambda表达式使用时很多地方都没写,那它怎么推断出来参数类型和返回值类型的?因为我们在创建函数的时候已经指定了:

// 已经把参数的参数类型和返回值类型声明了
fun setOnClickListener(onClick: (View) -> Unit) {
	this.onClick = onClick
}

那如果用lambda将函数赋值给一个变量:

val d = fun(param: Int): String {
	return param.toString()
}

// 不允许,因为kotlin没办法从上下文推断出类型
val d = {
	return it.toString()
}

// 如果出于场景需要就想用lambda省略参数类型,那你需要在赋值的变量加上传递的参数类型和返回值类型
val d: (Int) -> String = {
	it.toString()
}

另外,lambda表达式不是用 return 返回而是用代码直接返回的。在lambda中使用 return,它将会作为外层函数的返回值来直接结束外层函数:

// 你的想法是将 it.toString() 返回给变量 d
val d: (Int) -> String = {
	return it.toString() // IDE会提示 `'return' is not allowed here
}

实际上它等价于:

// 伪代码
fun a(): String {
	return it.toString()
}

在kotlin的lambda中会默认将最后一句代码作为返回值。

1.5 匿名函数和Lambda是什么?

匿名函数既可以作为参数传递,也可以赋值给变量,刚才我们说过函数是不能作为参数传递的,也不能赋值给变量,那为什么匿名函数就可以?因为匿名函数它不是函数,而是一个对象,是一个函数类型的对象。它和 ::b 双冒号加函数名是一个东西,和函数不是。

同理,lambda其实也是一个函数类型的对象而已,你能怎么使用双冒号加函数名,就能怎么使用匿名函数和lambda,这就是kotlin的匿名函数以及lambda表达式的本质。

a(fun(param: Int): String {
	return param.toString()
})

val d = fun(param: Int): String {
	return param.toString()
}

a { it.toString() }

val d = { param: Int -> 
	param.toString()
} 

kotlin的lambda和java的lambda是不一样的,java的lambda是一种便捷写法让你在使用上更方便但并没有在功能上有所改变,而kotlin的lambda是实实在在的对象。

在kotlin中,了解了函数并不能传递,传递的是对象匿名函数和lambda表达式其实都是对象后,去写kotlin的高阶函数和理解它们也会变得更加简单。

2 集合的函数式API

2.1 基础:filter和map

filter 函数遍历集合并选出应用给定lambda后会返回true的那些元素:

>>>val list = listOf(1, 2, 3, 4)
>>>println(list.filter { it % 2 == 0 }) // 筛选打印出偶数

输出:
[2, 4]

map 函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合:

>>>>val list = listOf(1, 2, 3, 4)
// 结果是一个新集合,包含的元素个数不变,但是每个元素根据给定的判断式做了变换
>>>pritln(list.map { it * it })

输出:
[1, 4, 9, 6]

lambda虽然简单简洁,但也要注意执行的lambda的原理避免多余的计算:

// filter传递每一个元素判断后,filter的元素又会去执行people.maxBy()遍历,people.maxBy()放在里面是多余的计算
people.filter { it.age == people.maxBy(Person::age)?.age } 

// 先查找出最大年龄在去执行filter减少计算
val maxAge = people.maxBy(Person::age)?.age
people.filter { it.age == maxAge }

对map应用过滤和变换函数:

>>>val numbers = mapOf(0 to "zero", 1 to "one")
>>>println(numbers.mapValues { it.value.toUpperCase() })

输出:
{0=ZERO, 1=ONE}

键和值分别由各自的函数来处理。filterKeysmapKeys 过滤和变换map的键,而另外的 filterValuesmapValues 过滤和变换对应值。

2.2 all、any、count和find:对集合应用判断式

检查集合中的所有元素是否都复合某个条件(或者它的变种,是否存在符合的元素)。它们是通过 allany 函数表达的。count 函数检查有多少个元素满足判断式,而 find 函数返回第一个符合条件的元素。

val canBeInClub27 = { p: Person -> p.age <= 27 }

如果你对是否所有元素都满足判断式感兴趣,应该使用 all 函数:

>>>val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>>println(people.all(canBeInClub27))

输出结果:
false

如果你需要检查集合中是否至少存在一个匹配的元素,那就用 any

>>>println(people.any(canBeInClub27))

输出结果:
true

注意,!all 加上某个条件,可以用 any 加上这个条件的取反来替换,应该尽量用确定的判断来表达:

>>>val list = listOf(1, 2, 3)
>>>prinltn(!list.all { it == 3 })
>>>println(list.ayn { it != 3 }) // 应该使用这种方式

如果你想知道有多少个元素满足了判断式,使用 count

>>>val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>>println(people.count(canBeInClub27))

输出结果:
1

count 方法容易被遗忘,然后通过过滤集合之后再取大小来实现它:

// 这种情况下,filter会创建一个中间集合并用来存储所有满足判断式的元素
// 而count只是跟踪匹配元素的数量,不关心元素本身,所以更高效
>>>println(people.filter(canBeInClub27).size)

要找到一个满足判断式的元素,使用 find 函数:

>>>val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>>println(people.find(canBeInClub27))

输出结果:
Person(name=Alice, age=27)

如果有多个匹配的元素就返回其中第一个元素;或者返回null,如果没有一个元素能满足判断式。find 还有一个同义方法 firstOrNull

2.3 groupBy:把列表转换成分组的map

groupBy 函数能把所有元素按照不同的特征划分成不同的分组。操作的结果是一个map:

>>>val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31))
>>>println(people.groupBy { it.age })

输出结果:
{29=[Person(name=Bob, age=29)],
31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}

每一个分组都是存储在一个列表中,上面的例子结果类型就是 Map<Int, List<Person>>。可以使用像 mapKeysmapValues 这样的函数对这个map做进一步的修改。

2.4 flatMap和flatten:处理嵌套集合中的元素

class Book(val title: String, val authors: List<String>)

books.flatMap { it.authors }.toSet()

flatMap 函数做了两件事情:首先根据作为实参给定的函数对集合中的每个元素做变换(或者说映射),然后把多个列表合并(或者说平铺)成一个列表。

>>>val strings = listOf("abc", "def")
>>>println(strings.flatMap { it.toList() })

输出结果:
[a, b, c, d, e, f]

在这里插入图片描述

>>>val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")), 
					  Book("Mort", listOf("Terry Pratchett")),
					  Book("Good Omens", listOf("Terry Pratchett", "Neil Gaiman")))
>>>println(books.flatMap { it.authors }.toSet())

输出结果:
[Jasper Fforde, Terry Pratchett, Neil Gaiman]

当你卡壳在元素集合的集合不得不合并成一个的时候,你可能会想起 flatMap 来。注意,如果你不需要做任何变换,只是需要平铺一个集合,可以使用 flatten 函数:listOfLists.flatten()

3 惰性集合操作:序列

people.map(Person::name).filter { it.startWith("A") }

kotlin标准库参考文档有说明,flitermap 都会返回一个列表。这意味着上面例子中的链式调用会创建两个列表:一个保存 filter 函数的结果,另一个保存 map 函数的结果。如果数量庞大时调用将会非常低效。

kotlin惰性集合操作的入口就是 Sequence 接口。这个接口表示的就是一个可以逐个列举元素的元素序列。Sequence 只提供了一个方法 iterator,用来从序列中获取值。

Sequence 接口的强大之处在于其操作的实现方式。序列中的元素求值时惰性的。因此,可以使用序列更高效地对集合元素执行链式操作,而不需要创建额外的集合来保存过程中产生的中间结果。

扩展函数 asSequence 把任意集合转换成序列,调用 toList 来做反向的转换。序列转换集合的场景是,如果你只需要迭代序列中的元素,可以直接使用序列。如果你要用其他的API方法,比如用下标访问元素,那么你需要把序列转换成列表。

3.1 执行序列操作:中间和末端操作

序列操作分为两类:中间的和末端的。一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。而一次末端操作返回的是一个结果,这个结果可能是集合、元素、数字,或者其他从初始集合的变换序列中获取的任意对象。

// map、filter都是中间操作,toList()是末端操作
sequence.map { ... }.filter { ... }.toList()

>>>listOf(1, 2, 3, 4).asSequence()
			.map { print("map($it)  "); it * it }
			.filter { print("filter($it)  "; it % 2 == 0 }
			.toList() // 没有末端操作,map和filter被延期了,只有调用toList()末端操作获取结果时才会调用map和filter

输出结果:
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)

值得注意的是计算执行顺序。

  • 普通集合调用 mapfilter:在每个元素上调用 map 函数,然后在结果序列的每个元素上再调用 filter 函数(即先对集合的所有元素都执行了 map 后,再将map后的集合迭代给 filter )。

  • 序列集合调用 mapfilter:所有操作是按顺序应用在每一个元素上:处理完第一个元素(先 mapfilter),然后完成第二个元素的处理,以此类推。

在这里插入图片描述

3.2 创建序列

generateSequence 函数:给定序列中的前一个元素,这个函数会计算出下一个元素:

>>>val naturalNumbers = genrateSequence(0) { it + 1 }
>>>val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>>println(numbersTo100.sum()) // 调用sum时才开始执行

输出结果:
5050

4 使用java函数式接口

4.1 把lambda当作参数传递给java方法

void postponeComputation(int delay, Runnable computation);

在kotlin中,可以调用它并把一个lambda作为实参传给它。编译器会自动把它转换成一个Runnable实例:

postponeComputation(1000) { println(42) }

注意,当我们说“一个Runnable实例”时,指的是“一个实现了Runnable接口的匿名类的实例”。

通过显式地创建一个实现了Runnable的匿名对象也能达到同样的效果:

postponeComputation(1000, object: Runnable {
	override fun run() {
		println(42)
	}
})

但是这里有一点不一样。当你显式地声明对象时,每次调用都会创建一个新的实例。使用lambda的情况不同:如果lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用:

// 使用lambda每次调用该函数,只会重用一个Runnable,不会创建新的实例
postponeComputation(1000) { println(42) } 
等价于
val runnable = Runnable { println(42) }
fun handleComputation() {
	postponeComputation(1000, runnable)
}

如果lambda从包围它的作用域中捕捉了变量,每次调用就不再可能重用同一个实例了。这种情况下,每次调用时编译器都要创建一个新的对象,其中存储着被捕捉的变量的值。

fun handleComputation(id: String) {
	postponeComputation(1000) { println(id) } // lambda捕捉了id,每次调用都会创建Runnable
}

lambda的实现细节:

自kotlin1.0起,每个lambda表达式都会被编译成一个匿名类,除非它是一个内联lambda。
后续版本计划支持生成java8字节码。一旦实现,编译器就可以避免为每一个lambda表达式都生成一个独立的.class文件。
如果lambda捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段,而且每次调用都会创建一个这个匿名类的新实例。
否则,一个单例就会被创建。类的名称由lambda声明所在的函数名字称加上后缀衍生出来:

class HandleComputation$1(val id: String): Runnable {
	override fun run() {
		println(id)
	}
}
fun handleComputation(id: String) {
	postponeComputation(1000, HandleComputation$1(id)) // 底层创建的是一个特殊类的实例,而不是一个lambda
}

4.2 SAM构造方法:显式地把lambda转换成函数式接口

SAM构造方法是编译器生成的函数,让你执行从lambda到函数式接口实例的显式转换。可以在编译器不会自动应用转换的上下文中使用它。例如,如果有一个方法返回的是一个函数式接口的实例,不能直接返回一个lambda,要用SAM构造方法把它包装起来。

fun createAllDoneRunnable(): Runnable {
	return Runnable { println("All done!") }
}
>>>createAllDoneRunnable().run()

SAM构造方法的名称和底层函数式接口的名称一样。SAM构造方法只接收一个参数——一个被用作函数式接口单抽象方法体的lambda——并返回实现了这个接口的类的一个实例。

除了返回值外,SAM构造方法还可以用在需要把从lambda生成的函数式接口实例存储在一个变量中的情况。

val listener = OnClickListener { view ->
	val text = when(view.id) {
		R.id.button1 -> "First button"
		R.id.button2 -> "Second button"
		else -> "Unknown button"
	}
	toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)

lambda和添加/移除监听器:

注意lambda内部没有匿名对象那样的 `this`:
没有办法引用到lambda转换成的匿名类实例。
从编译器的角度来看,lambda是一个代码块,不是一个对象,而且也不能把它当成对象引用。lambda中的 `this` 引用指向的是包围它的类。

尽管方法调用中的SAM转换一般都自动生成,但是当把lambda作为参数传给一个重载方法时,也有编译器不能选择正确的重载情况。这时,使用显式的SAM构造方法是解决编译器错误的好方法。

5 内联函数

kotlin在集合API中大量使用了Lambda,这确实使得我们在对集合进行操作的时候优雅了许多。但是这种方式的代价就是,在kotlin中使用Lambda表达式会带来一些额外的开销。

kotlin中的内联函数之所以被设计出来,主要是为了优化kotlin支持Lambda表达式之后所带来的开销。然而,在java中我们却似乎不需要特别关注这个问题,因为在java 7之后,JVM引入了一种叫做 invokedynamic 技术自动帮助我们优化Lambda。

5.1 优化Lambda开销

kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含一个 invoke(),作为Lambda的调用方法,每次调用的时候还会创建一个新对象。Lambda虽然简洁,但是额外增加的开销也不少。

5.1.1 invokedynamic

在Java 7之后通过 invokedynamic 技术实现了在运行期才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码。这样的好处是:

  • 由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少

  • 与kotlin编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在jdk库的实现,这极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对翻译策略不断优化升级

  • JVM天然支持了针对该方式的Lambda表达式的翻译和优化,这也意味着开发者在书写Lambda表达式的同时,可以完全不用关心这个问题,极大提升了开发的体验

5.1.2 inline:内联函数

kotlin在一开始就需要兼容Android最主流的java 6,这导致它无法通过invokedynamic来解决Android平台的Lambda开销问题。

通过关键字 inline 修饰方法成为内联函数,内联函数在编译期会被嵌入每一个被调用的地方,以减少额外生成的匿名类数以及函数执行的时间开销。

fun main(args: Array<String>) {
	foo {
		println("dive into kotlin...")
	}
}

fun foo(block: () -> Unit) {
	println("before block")
	block()
	println)"end block")
}

将上面的代码反编译为java:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
   this.foo((Function0)null.INSTANCE);
}

public final void foo(@NotNull Function0 block) {
   Intrinsics.checkParameterIsNotNull(block, "block");
   String var2 = "before block";
   boolean var3 = false;
   System.out.println(var2);
   block.invoke();
   var2 = "end block";
   var3 = false;
   System.out.println(var2);
}

调用 foo() 就会产生一个 Function0 类型的 block 类,通过 invoke() 来执行,这会增加额外的生成类和调用开销。在正常情况下你会认为不就创建一个对象这没什么,但是如果 lambda 是在一个循环中执行,就会快速产生和销毁临时对象,容易引发性能问题:

fun main(args: Array<String>) {
	for (i in 1..100) {
		foo { // lambda 在循环不断创建临时对象
			println("dive into kotlin...")
		}
	}
}

fun foo(block: () -> Unit) {
	println("before block")
	block()
	println)"end block")
}

为了避免上面怼问题,现在添加 inline 修饰符:

fun main(args: Array<String>) {
    foo {
        println("dive into kotlin...")
    }
}

inline fun foo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

再反编译为java:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
   int $i$f$foo = false;
   String var4 = "before block";
   // block函数体开始粘贴
   boolean var5 = false;
   System.out.println(var4);
   int var6 = false;
   String var7 = "dive into kotlin...";
   boolean var8 = false;
   System.out.println(var7);
   // block函数结束粘贴
   var4 = "end block";
   var5 = false;
   System.out.println(var4);
}

public final void foo(@NotNull Function0 block) {
   int $i$f$foo = 0;
   Intrinsics.checkParameterIsNotNull(block, "block");
   String var3 = "before block";
   boolean var4 = false;
   System.out.println(var3);
   block.invoke();
   var3 = "end block";
   var4 = false;
   System.out.println(var3);
}

通过内联函数 foo 函数体代码及调用的Lambda代码都粘贴到了相应调用的位置。这样就彻底消除额外调用,从而节约了开销。

内联函数典型的应用场景是kotlin的集合类,可以看下操作符的源码:

inline fun <T, R> Array<out T>.map(transform: (T) -> R): List<R>

inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T>

但内联函数不是万能的,以下情况应避免使用内联函数:

  • JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联化,所以普通函数不需要添加 inline,否则只会让字节码变得更加复杂

  • 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量

  • 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把它们声明为 internal

5.2 noinline:避免参数被内联

在现实中情况比较复杂,有一种可能是函数需要接收多个参数,但我们只想对其中部分Lambda内联,其他的则不内联。

通过 noinline 关键字,我们可以把它加在不想要内联的参数开头,该参数便不会具有内联的效果。

fun main(args: Array<String>) {
    foo ({
        println("I am inlined...")
    }, {
        println("I'm not inlined...")
    })
}

inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {
    println("before block")
    block1()
    block2()
    println("end block")
}

反编译为java:

public final void main(@NotNull String[] args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
   Function0 block2$iv = (Function0)null.INSTANCE;
   int $i$f$foo = false;
   String var5 = "before block";
   boolean var6 = false;
   System.out.println(var5);
   int var7 = false;
   // block1被内联了
   String var8 = "I am inlined...";
   boolean var9 = false;
   System.out.println(var8);
   // block2原样没被内联
   block2$iv.invoke();
   var5 = "end block";
   var6 = false;
   System.out.println(var5);
}

public final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {
   int $i$f$foo = 0;
   Intrinsics.checkParameterIsNotNull(block1, "block1");
   Intrinsics.checkParameterIsNotNull(block2, "block2");
   String var4 = "before block";
   boolean var5 = false;
   System.out.println(var4);
   block1.invoke();
   block2.invoke();
   var4 = "end block";
   var5 = false;
   System.out.println(var4);
}

5.3 非局部返回

kotlin中的内联函数除了优化Lambda之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。

fun main(args: Array<String>) {
	foo()
}

fun foo() {
	println("before local return")
	localReturn()
	println("after local return")
	return
}

fun localReturn() {
	return
}

输出:
before local return
after local return

localReturn() 执行后,其函数体中的 return 只会在该函数的局部生效,所以 localReturn() 之后的 println() 依旧生效。

我们再把函数换成Lambda:

fun main(args: Array<String>) {
	foo {
		return // 错误,lambda 不允许存在 return 关键字
	}
}

fun foo(returning: () -> Unit) {
	println("before local return")
	returning()
	println("after local return")
	return
}

输出:
Error:(2, 11) Kotlin: 'return' is not allowed here

编译报错了,在kotlin中,正常情况下Lambda表达式不允许存在 return 关键字。

那我们换成内联函数:

fun main(args: Array<String>) {
	foo {
		return // lambda 是内联函数,允许使用 return 关键字
	}
	println("main function continue")
}
inline fun foo(returning: () -> Unit) {
	println("before local return")
	returning()
	println("after local return")
	return
}

输出:
before local return

编译通过了,而且 return 后直接让 foo() 退出了执行。其实细想以下也很简单,内联函数是将编译是是将代码粘贴到过去的,相当于 return 是直接暴露在 main 函数中。这个也就是非局部返回。上面的代码效果如下伪代码:

fun main(args: Array<String>) {
	// foo {
	//	return // lambda 是内联函数,允许使用 return 关键字
	//}
	println("before local return")
	return // 内联函数代码粘贴后直接 return 将外部函数也返回
	println("after local return")
	return
}

还有一种等效的写法:

fun main(args: Array<String>) {
	foo {
		return@foo // 不会中断外部函数 main() 的执行
		println("lambda continue") // lambda 已经被返回,lambda 后续代码不会被执行
	}
	println("main function continue")
}

fun foo(returning: () -> Unit) {
	println("before local return")
	returning()
	println("after local return")
	return
}

输出结果:
before local return
after local return
main function continue

非局部返回在循环控制中特别有用,比如kotlin的 forEach,它接收的就是一个Lambda参数,由于它是一个内联函数,所以可以直接在它调用的Lambda中执行 return 退出上一层程序:

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3, 4, 0, 5)
    forEachFunction(list)
    println("main function continue")
}

fun forEachFunction(list: List<Int>): Boolean {
    list.forEach {
        println("value is $it")
        if (it == 0) {
            println("value is 0, break for each function")
            return true // 中断了外部函数 forEachFunction 的执行
        }
    }
    return false
}

输出结果:
value is 1
value is 2
value is 3
value is 4
value is 0
value is 0, break for each function
main function continue

5.4 crossinline

值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有 return 的Lambda参数产生破坏,可以使用 crossinline 关键字修饰该参数,从而杜绝此类问题的发生:

fun main(args: Array<String>) {
	foo { return }
}

inline fun foo(crossinline returning: () -> Unit {
	println("before local return")
	returning()
	println("after local return")
	return
}

输出:
Error(2, 11) Kotlin: 'return' is not allowed here

5.5 内联函数总结

  • 在 kotlin 中,内部 lambda 是不允许中断外部函数执行的

  • inline 的 lambda 可以中断外部函数调用

  • crossinline 不允许 inline 的 lambda 中断外部函数执行

  • noinline 拒绝内联

6 常用的操作符

元素操作类:

操作符作用
contains判断是否有指定元素
elementAt返回对应的元素,越界会抛 IndexOutOfBoundsException
firstOrNull返回符合条件的第一个元素,没有返回 null
lastOrNull返回符合条件的最后一个元素,没有返回 null
indexOf返回指定元素的下标,没有返回 -1
singleOrNull返回符合条件的单个元素,如没有符合或超过一个,返回 null

判断类:

操作符作用
any判断集合中是否有满足条件的元素
all判断集合中的元素是否都满足条件
none判断集合中是否都不满足条件,是则返回 true
count查询集合中满足条件的元素个数
reduce从第一项到最后一项进行累计

过滤类:

操作符作用
filter过滤掉所有满足条件的元素
filterNot过滤掉所有不满足条件的元素
filterNotNull过滤 null
take返回前 n 个元素

转换类:

操作符作用
map转换为另一个集合
mapIndexed除了转换为另一个集合,还可以拿到下标 index
mapNotNull执行转换前过滤掉为 null 的元素
flatMap自定义逻辑合并两个集合
groupBy按照某个条件分组,返回 map

排序类:

操作符作用
reversed反序
sorted升序
sortedBy自定义排序
sortedDescending降序
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值