高阶函数
高阶函数定义
高阶函数是指可以接收其他函数作为参数,或者返回一个函数的函数。在许多编程语言中,高阶函数都是一种重要的编程方式,因为它们提供了更高的抽象层次,使得代码更加模块化、易于理解和维护。
为什么一个函数能接收另一个函数作为参数呢?因为Kotlin中新增了函数类型,将这种函数类型添加到一个函数的参数声明或者返回值声明当中,那么该函数就是一个高阶函数了。
函数类型
函数类型定义的基本规则如下:
(String, Int) -> Unit
既然是定义一个函数类型,那么最关键的就是要声明该函数接受什么参数,以及它的返回值是什么。
- ->左边的部分就是用来声明该函数接收什么,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。
- ->右边部分用于声明该函数返回值是什么类型,如果没有返回值就使用Unit,Unit类似于Java的void表示无返回值。
高阶函数使用
private fun doOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
val result1 = doOperation(10, 5) { x, y -> x + y }
Log.d(TAG, "result1:$result1") //输出15
定义一个 doOperation 函数,它接受两个整数 x 和 y,以及一个函数 operation。这个函数接受两个整数参数,并返回一个整数。我们可以将一个 Lambda 表达式作为 operation 参数传递给 doOperation 函数。
在上述例子中,将一个 Lambda 表达式 { x, y -> x + y } 传递给了 doOperation 函数,这个 Lambda 表达式表示将两个整数相加的操作。最后,doOperation 函数返回了相加后的结果 15,并打印了这个结果。
除了像上述例子中展示的那样,使用 Lambda 表达式作为函数参数外,Kotlin 还支持使用函数引用(function reference)来传递函数。例如,我们可以使用 :: 运算符引用一个已经定义好的函数,然后将它作为高阶函数的参数传递。
private fun add(x: Int, y: Int) = x + y
val result2 = doOperation(10, 5, ::add)
Log.d(TAG, "result2:$result2") //输出15
定义了一个名为 add 的函数,它接受两个整数参数,并返回它们的和。使用 :: 运算符引用这个函数,然后将它作为 doOperation 函数的参数传递。doOperation 函数将调用 add 函数,并返回相加后的结果 15
常见的高阶函数
- map函数:将集合中的每个元素都应用一个函数,并返回一个新的集合。
//map:将集合中的每个元素都应用一个函数,并返回一个新的集合。
val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it }
Log.d(TAG, "squaredNumbers:$squaredNumbers") //输出[1, 4, 9, 16, 25]
- filter函数:过滤,返回一个新的集合,其中包含满足给定条件的所有元素。
//filter:返回一个新的集合,其中包含满足给定条件的所有元素。
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumber = numbers.filter { it % 2 == 0 }
Log.d(TAG, "evenNumber:$evenNumber") //输出[2, 4]
- reduce函数:将集合中的所有元素合并成一个值,具体的合并方式由指定的函数决定。
//reduce:将集合中的所有元素合并成一个值,具体的合并方式由指定的函数决定。
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, i ->
Log.d(TAG, "acc:$acc") //依次输出1,3,6,10
acc + i
}
Log.d(TAG, "sum:$sum") //输出15
- fold函数:与 reduce 函数类似,但是可以指定一个初始值。
//fold:与 reduce 函数类似,但是可以指定一个初始值。
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.fold(5) { acc, i ->
Log.d(TAG, "acc:$acc") //依次输出5,6,8,11,15
acc + i
}
Log.d(TAG, "sum:$sum") //输出20
- forEach函数:对集合中的每个元素执行指定的操作,没有返回值。
//forEach:对集合中的每个元素执行指定的操作,没有返回值。
val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach {
Log.d(TAG, "$it") //依次输出1,2,3,4,5
}
- flatMap函数:对集合中的每个元素应用一个函数,并将结果合并成一个新的集合。
//flatMap:对集合中的每个元素应用一个函数,并将结果合并成一个新的集合。
val words = listOf("hello", "world", "kotlin")
val chars = words.flatMap { it.toList() }
Log.d(TAG, "chars:$chars") //输出[h, e, l, l, o, w, o, r, l, d, k, o, t, l, i, n]
乍一看是不是觉得flatMap函数和map函数差不多,那就来看看区别:
//flatMap:对集合中的每个元素应用一个函数,并将结果合并成一个新的集合。
val words = listOf("hello", "world", "kotlin")
val chars = words.flatMap { it.toList() }
Log.d(TAG, "chars:$chars") //输出[h, e, l, l, o, w, o, r, l, d, k, o, t, l, i, n]
val charsMap = words.map { it.toList() }
Log.d(TAG, "charsMap:$charsMap")//输出[[h, e, l, l, o], [w, o, r, l, d], [k, o, t, l, i, n]]
flat本意平的、扁平的,flatMap函数将结果展平成一个新的集合,消除了集合中的层级结构。
- groupBy函数:根据指定的键将集合分组,返回一个 Map 对象。返回结果Map中,key为指定的键,value为具有同一个key的对象所组成的List。
data class Person(val name: String, val age: Int)
//groupBy:根据指定的键将集合分组,返回一个 Map 对象。
val people = listOf(
Person("Alice", 20),
Person("Bob", 22),
Person("Charlie", 20),
Person("David", 25)
)
val groupedPeople = people.groupBy { it.age }
Log.d(TAG, "groupedPeople:$groupedPeople") //{20=[Person(name=Alice, age=20), Person(name=Charlie, age=20)], 22=[Person(name=Bob, age=22)], 25=[Person(name=David, age=25)]}
- sortedBy函数:按照指定的排序规则对集合进行排序,并返回一个新的集合。
data class Person(val name: String, val age: Int)
//sortedBy:按照指定的排序规则对集合进行排序,并返回一个新的集合。
val people = listOf(
Person("Alice", 20),
Person("Bob", 22),
Person("Charlie", 20),
Person("David", 25)
)
val sortedPeople = people.sortedBy { it.age }
Log.d(TAG, "sortedPeople:$sortedPeople") //[Person(name=Alice, age=20), Person(name=Charlie, age=20), Person(name=Bob, age=22), Person(name=David, age=25)]
- takeWhile函数:返回集合中从开始位置开始的连续元素,直到遇到第一个不满足给定条件的元素。
//takeWhile:返回集合中从开始位置开始的连续元素,直到遇到第一个不满足给定条件的元素。
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers.takeWhile { it <= 3 }
Log.d(TAG, "result:$result") // 输出 [1, 2, 3]
- any函数:判断集合中是否存在满足给定条件的元素,返回一个布尔值。
//any:判断集合中是否存在满足给定条件的元素,返回一个布尔值。
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers.any { it % 2 == 0 }
Log.d(TAG, "result:$result") // 输出true
- let函数:接收一个lambda表达式,其返回值为lambda表达式中最后一行的结果,let函数的参数则被传递到lambda表达式中。
该函数主要用于避免空指针异常,简化对可空对象的判空操作。
//let:接收一个lambda表达式,其返回值为lambda表达式中最后一行的结果,let函数的参数则被传递到lambda表达式中。
val str: String= "Hello World"
str?.let {
// str不为null的情况下才会执行下面的语句
Log.d(TAG, "length:${it.length}") // 输出11
}
- also函数:also 函数和 let 函数很像,但是它返回的是调用对象本身,而不是最后一行的结果。also 函数的参数同样会传递到lambda表达式中。
//also:接收一个lambda表达式,其返回值为调用对象本身,而不是最后一行的结果,also函数的参数则被传递到lambda表达式中。
val list = mutableListOf<Int>()
list.also {
// 对list进行一些操作,返回的是list本身
it.add(1)
it.add(2)
}.also {
// 进一步操作list,返回的还是list本身
it.add(3)
}
Log.d(TAG, "list:$list") // 输出[1, 2, 3]
- run函数:run 函数和 let 函数非常相似,它也是接收一个lambda表达式,返回的是lambda表达式中最后一行的结果,但它和 let 的区别在于它的调用对象可以通过 this 关键字访问,而不是通过lambda表达式的参数。
//run:接收一个lambda表达式,其返回值为lambda表达式中最后一行的结果,它的调用对象可以通过 this 关键字访问,而不是通过lambda表达式的参数。
val str = "Hello World"
val length = str.run {
// 这里可以通过 this 访问 str 对象
println(this) // 输出 "Hello World"
length
}
Log.d(TAG, "length:$length") // 输出 11
- apply函数:apply 函数和 also 函数非常相似,它同样返回调用对象本身,但是它不接受lambda表达式的参数,而是在lambda表达式中直接操作调用对象。
data class Person(var name: String, var age: Int)
//apply:接收一个lambda表达式,返回调用对象本身,但是它不接受lambda表达式的参数,而是在lambda表达式中直接操作调用对象。
val penson = Person("Alice", 20)
penson.apply {
name = "Bob"
age = 22
}
Log.d(TAG, "name:${penson.name}") // 输出 Bob
Log.d(TAG, "age:${penson.age}") // 输出 22
- with函数:with函数不是扩展函数,而是一个独立函数,它接收两个参数:一个对象和一个lambda表达式。lambda表达式中的代码可以直接访问该对象的属性和方法,从而省去了在每个语句中重复引用对象的麻烦。
data class Person(var name: String, var age: Int)
//with:with函数不是扩展函数,而是一个独立函数,它接收两个参数:一个对象和一个lambda表达式。lambda表达式中的代码可以直接访问该对象的属性和方法,从而省去了在每个语句中重复引用对象的麻烦。
val penson = Person("Alice", 20)
with(penson) {
name = "Bob"
age = 22
}
Log.d(TAG, "name:${penson.name}") // 输出 Bob
Log.d(TAG, "age:${penson.age}") // 输出 22
函数引用的使用
函数引用是指将一个函数作为参数传递或者将其赋值给一个变量或者属性的方式。函数引用可以使代码更加简洁、易于阅读和维护。
在介绍高阶函数的使用时,提到了可以使用:: 运算符,函数引用(function reference)来传递函数。
常见引用方法
- 引用顶层函数:
在介绍高阶函数的使用时,使用了这种方法。 - 引用对象的成员函数:
data class Person(var name: String, var age: Int)
val peoples = listOf(Person("Alice", 29), Person("Bob", 31))
val names = peoples.map(Person::name)
Log.d(TAG, "names:$names") // 输出 [Alice, Bob]
- 引用构造函数:
data class Person(var name: String, var age: Int)
val createPerson = ::Person
val person = createPerson("Alice", 29)
Log.d(TAG, "person:$person") // 输出 Person(name=Alice, age=29)
内联函数
高阶函数的实现原理
先来学习一下高级函数的实现原理,以介绍高阶函数的使用时的例子为例:
private fun transformFunction() {
val result = doOperation(10, 5) { x, y -> x + y }
Log.d(TAG, "result:$result") // 输出 15
}
private fun doOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
我们知道Kotlin代码最终会编译成Java字节码的,而Java中是没有高阶函数概念的,其实Kotlin编译器最终会把Kotlin中高阶函数的语法转换成Java支持的语法结构,上述的Kotlin代码会被转换成如下Java代码:
private final void transformFunction() {
int result = doOperation(10, 5, MainActivity$transformFunction$result.INSTANCE);
Log.d(this.TAG, "result:" + result);
}
private final int doOperation(int x, int y, Function2<super Integer, super Integer, Integer> function2) {
return function2.invoke(Integer.valueOf(x), Integer.valueOf(y)).intValue();
}
final class MainActivity$transformFunction$result extends Lambda implements Function2<Integer, Integer, Integer> {
public static final MainActivity$transformFunction$result INSTANCE = new MainActivity$transformFunction$result();
MainActivity$transformFunction$result() {
super(2);
}
public final Integer invoke(int x, int y) {
return Integer.valueOf(x + y);
}
@Override // kotlin.jvm.functions.Function2
public /* bridge */ /* synthetic */ Integer invoke(Integer num, Integer num2) {
return invoke(num.intValue(), num2.intValue());
}
}
需要注意的是,原来传入的Lambda表达式在底层会创建一个类继承自Lambda,然后在内部生成一个静态实例对象,用该静态实例对象来代替Lambda表达式,这也就说明我们每使用一次高阶函数就会创建一个静态实例对象,当然会带来额外的内存和性能开销(有的时候Lambda表达式会被匿名类对象所代替,每使用一次高阶函数就会创建一个匿名类对象)。
而Kotlin中的内联函数就是为了解决这个问题的,它可以将使用Lambda表达式运行时的开销完全消除。
内联函数的使用及原理
内联函数的使用非常简单,就是在定义的高阶函数前加上inline关键字修饰:
private inline fun doOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
内联函数的原理也很简单:Kotlin编译器在编译时把内联函数内代码自动替换到要调用的地方,这样就解决了运行时的内存开销。
主要分为两个步骤
步骤一:将Lambda表达式替换到高阶函数里函数类型参数调用的地方
步骤二:将高阶函数全部代码替换到函数调用的地方
最终,Kotlin代码被转换成Java代码之后是这样的:
private final void transformFunction() {
int result = 10 + 5;
Log.d(this.TAG, "result:" + result);
}
private final int doOperation(int x, int y, Function2<super Integer, super Integer, Integer> function2) {
return function2.invoke(Integer.valueOf(x), Integer.valueOf(y)).intValue();
}
效果一目了然。
noinline和crossinline
noinline
一个高阶函数中接收两个或更多函数类型的参数,如果高阶函数被inline修饰了,那么所有函数类型的参数均会被内联,如果想某个函数类型的参数不被内联,可以用关键字noinline修饰。
inline fun test(block1: () -> Unit, noinline block2: () -> Unit) {
}
既然内联函数能消除Lambda表达式运行时带来的内存的额外开销,那么为什么还提供了一个noinline来排除内联呢?
- 原因一:内联函数类型的参数在编译期间会进行代码替换,所以内联的函数类型的参数算不上真正的参数,非内联的函数类型的参数可以作为真正的参数传递给任何函数。内联函数类型的参数只能传递给另一个内联函数。这也是它最大的局限性。
- 原因二:内联函数和非内联函数有一个重要的区别:内联函数所引用的Lambda表达式中可以使用return来进行函数的返回,而非内联函数只能进行局部返回。
来看一个例子:
private fun transformFunction() {
Log.d(TAG, "begin")
printString("") {
Log.d(TAG, "lambda begin")
if (it.isEmpty()) return@printString
Log.d(TAG, "lambda end")
}
Log.d(TAG, "end")
}
private fun printString(str: String, block: (String) -> Unit) {
Log.d(TAG, "printString begin")
block(str)
Log.d(TAG, "printString end")
}
由于printString为非内联函数,里面Lambda表达式中只能使用return@printString来进行一个局部返回,而不能使用return来进行整个函数的返回。
转换成Java代码之后:
private final void transformFunction() {
Log.d(this.TAG, "begin");
printString("", new MainActivity$transformFunction(this));
Log.d(this.TAG, "end");
}
private final void printString(String str, Function1<super String, Unit> function1) {
Log.d(this.TAG, "printString begin");
function1.invoke(str);
Log.d(this.TAG, "printString end");
}
public final class MainActivity$transformFunction extends Lambda implements Function1<String, Unit> {
final /* synthetic */ MainActivity thisbegin
printString begin
lambda begin
printString end
end
;
/* JADX INFO: Access modifiers changed from: package-private */
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public MainActivity$transformFunction(MainActivity mainActivity) {
super(1);
this.this private inline fun printString(str: String, block: (String) -> Unit) {
Log.d(TAG, "printString begin")
block(str)
Log.d(TAG, "printString end")
}
= mainActivity;
}
@Override // kotlin.jvm.functions.Function1
public /* bridge */ /* synthetic */ Unit invoke(String str) {
invoke2(str);
return Unit.INSTANCE;
}
/* renamed from: invoke reason: avoid collision after fix types in other method */
public final void invoke2(String it) {
String str;
String str2;
Intrinsics.checkNotNullParameter(it, "it");
str = this.this private fun transformFunction() {
Log.d(TAG, "begin")
printString("") {
Log.d(TAG, "lambda begin")
if (it.isEmpty()) return
Log.d(TAG, "lambda end")
}
Log.d(TAG, "end")
}
private inline fun printString(str: String, block: (String) -> Unit) {
Log.d(TAG, "printString begin")
block(str)
Log.d(TAG, "printString end")
}
.TAG;
Log.d(str, "lambda begin");
if (it.length() == 0) {
return;
}
str2 = this.this private final void transformFunction() {
Log.d(this.TAG, "begin");
Log.d(this.TAG, "printString begin");
Log.d(this.TAG, "lambda begin");
if ("".length() == 0) {
return;
}
Log.d(this.TAG, "lambda end");
Log.d(this.TAG, "printString end");
String str$iv = this.TAG;
Log.d(str$iv, "end");
}
private final void printString(String str, Function1<super String, Unit> function1) {
Log.d(this.TAG, "printString begin");
function1.invoke(str);
Log.d(this.TAG, "printString end");
}
.TAG;
Log.d(str2, "lambda end");
}
}
当传入的字符串为空串时,Lambda表达式将局部返回,并不影响表达式之外的代码继续执行,输出结果如下:
begin
printString begin
lambda begin
现在将高阶函数printStr声明为内联函数:
crossinline
如果仍然是return@printString,输出结果将不变。但由于printStr是内联函数,因此可以在Lambda表达式中使用return进行整个函数的返回:
private inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
转换成Java代码之后:
private inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
输出结果如下:
将高阶函数声明成内联函数是一种良好的习惯,事实上绝大多数高阶函数是可以被声明成内联函数的,但是也有例外的情况:
上述代码在没有声明成内联函数前是完全可以运行的,但是声明为内联函数之后就会报错:
我们在内联函数runRunnable中创建一个runnable对象,并在Runnable的Lambda表达式中传入的函数类型参数,而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说上面代码是在匿名类中传入了函数类型的参数。
而内联函数所引用的Lambda表达式允许使用return进行整个函数的返回,但是由于我们是在匿名类中调用的函数类型参数,此时不能进行外层调用函数的返回,最多只能进行匿名类中的方法进行返回,因此就提示了上述错误。
也就是说:如果在高阶函数中创建了Lambda表达式或匿名类的实现,在这些实现中调用函数类型参数,此时再将高阶函数声明成内联,就会报上面的错误。这个时候就需要使用关键字crossinline:
crossinline关键字用于保证在Lambda表达式中一定不使用return关键字,这样冲突就不存在了。但是仍然可以使用return@runRunnable进行局部返回。总体来说,crossinline除了return用法不同外,仍然保留了内联函数的所有特性。