Kotlin-函数和Lambda表达式

单表达式函数

若函数只是返回单个表达式,则可以省略 { }return,直接在 = 号后指定函数体即可。

fun area(x: Int, y: Int): Int {
    return x * y
}
// 可写成
fun area(x: Int, y: Int): Int = x * y
// 对于单表达式函数而言,编译器可以推断出函数的返回值类型,故可以省略函数的返回值类型,可改为
fun area(x: Int, y: Int) = x * y

具名参数

在传参给函数时,可以显式指定形参名称进行传递。

  1. 函数定义
fun girth(width: Int, height: Int): Int {
    println("width: $width, height: $height")
    return 2 * (width + height)
}
  1. 函数调用
// 传统调用函数方式,根据位置传入参数
girth(2, 4)
// 根据参数名传入参数
girth(width = 2, height = 4)
// 使用具名参数时刻交换位置
girth(height = 4, width = 2)
// 部分使用具名参数,注意:具名参数之后一定也要是具名参数
girth(2, height = 4)

注意:上面注释说到,具名参数之后一定也要是具名参数。
这句话在 Kotlin 1.4 之前是对的,但是 Kotlin 1.4 针对这个地方做了优化:
只要你参数使用的顺序正确,你可以自由使用具名参数,没有位置的限制。
例如:test(a = 1, 2, 3, d = 4, 5, f = 6),这是可以正常编译通过的,但是在 Kotlin 1.4 之前版本就不行。


形参默认值

可直接在函数声明时为形参赋上默认值,用 = 号。

  1. 函数定义
// 形参默认值
fun sayHi(name: String = "孙悟空", message: String = "欢迎来到西游记") {
    println("${name}说:$message")
}

// 形参默认值,注意,不建议将有默认值的形参放在前面
fun sayBye(bye: Boolean = true, name: String) {
    println("$name ${if (bye) "说再见" else "不说再见"}")
}
  1. 函数调用
// 全部使用默认参数
sayHi() //孙悟空说:欢迎来到西游记
// message 使用默认参数
sayHi("猪八戒") //猪八戒说:欢迎来到西游记
// name 使用默认参数
sayHi(message = "我的扮演者是六小龄童") //孙悟空说:我的扮演者是六小龄童
// 都不使用默认参数
sayHi("曹操", "欢迎来到三国演义") //曹操说:欢迎来到三国演义
// 传入必传形参,由于必传参数在第二个,故当有默认值的形参不传时,必须显式指定形参名
sayBye(name = "孙悟空") //孙悟空 说再见
// 传统函数调用方式:两个参数都传
sayBye(false, "唐僧") //唐僧 不说再见
// 都使用具名参数,顺序可自定义
sayBye(name = "曹操", bye = true) //曹操 说再见

尾递归函数

当函数将调用自身作为它执行的最后一行代码,且递归调用后没有更多代码时,可使用尾递归语法。

  1. 尾递归不能在trycatchfinally块中使用;
  2. 尾递归函数需要使用tailrec修饰;
  3. 优点:编译器会对尾递归进行修改,将其优化成一个快速而且高效的基于循环的版本,这样就可以减少可能对内存的消耗。
  1. 函数定义
// 尾递归函数
// 计算阶乘,普通递归方式实现
fun fact(n: Int): Int {
    if (n == 1) {
        return 1
    }
    return n * fact(n - 1)
}

// 计算阶乘,使用尾递归函数
tailrec fun factRec(n: Int, total: Int = 1): Int =
    if (n == 1) total else factRec(n - 1, total * n)
  1. 函数调用
println(fact(4)) //24
println(factRec(4)) //24

个数可变的形参

在形参前添加vararg修饰,其本质就是一个数组。

  1. vararg参数可放在形参列表的任意位置,但必须保证只有一个
  2. vararg参数不在最后一个时,其他形参需使用具名参数
  3. 将数组的多个元素传给vararg参数,在数组参数前加 * 运算符
  1. 函数定义
// 个数可变的形参
fun test(a: Int, vararg books: String) {
    books.forEach {
        println(it)
    }
    println("$a")
}

// kotlin 允许将 vararg 参数放在形参列表的任意位置,但必须保证只有一个
fun test2(vararg books: String, a: Int) {
    books.forEach {
        println(it)
    }
    println("$a")
}
  1. 函数调用
// 可传入多个形参
test(2, "Java", "Kotlin")
// 当 vararg 不在最后一个时,其他形参需使用具名参数
test2(a = 1)
// 将数组的多个元素传给个数可变的参数,在数组参数前加 * 运算符
val books = arrayOf("Kotlin", "Swift")
test2(*books, a = 1)

函数重载

函数名相同,形参列表不同(形参个数或类型),就称为函数重载。

  1. 函数定义
// 定义一个无参函数
fun testOverload() {
    println("无参数的 testOverload() 函数")
}

// 定义一个传 String 的函数
fun testOverload(str: String) {
    println("重载 testOverload() 函数:$str")
}

// 定义一个传 Int 的函数,并返回字符串
fun testOverload(str: Int): String {
    println("重载 testOverload() 函数:$str,带返回值")
    return str.toString()
}

// 定义一个形参个数可变的函数
fun testOverload(vararg str: String) {
    println("重载 testOverload() 函数,参数为个数可变的形参:${str.contentToString()}")
}
  1. 函数调用
// 调用无参函数
testOverload() //无参数的 testOverload() 函数
// 调用带 String 的函数
testOverload("111") //重载 testOverload() 函数:111
// 调用带 Int 的函数,并返回字符串
testOverload(222) //重载 testOverload() 函数:222,带返回值
// 调用形参个数可变的函数
testOverload("333", "444") //重载 testOverload() 函数,参数为个数可变的形参:[333, 444]

虽然testOverload("111")函数既可匹配fun testOverload(str: String) { ... },又可匹配fun testOverload(vararg str: String) { ... },但是 Kotlin 会尽量执行最精确的匹配,所以会匹配调用 fun testOverload(str: String) { ... } 函数。

注意:Kotlin 大部分时候并不推荐重载形参个数可变的函数,因为这样做确实没有太大的意义,而且容易导致出错。


局部函数

之前所定义的函数都是在全局范围内定义的,它们都是全局函数。Kotlin 还支持在函数体内部定义函数,这种被放在函数体内部定义的函数就称为局部函数。

  1. 默认情况下,局部函数对外是隐藏的,它只能在其封闭函数内有效;
  2. 如果封闭函数返回局部函数,那么程序可以在其他作用域中使用局部函数。
  1. 函数定义
// 根据传入的 type 调用不同的局部函数
fun getMathFunc(type: String, nn: Int): Int {
    // 定义一个计算平方的局部函数
    fun square(n: Int): Int {
        return n * n
    }

    // 定义一个计算立方的局部函数
    fun cube(n: Int): Int {
        return n * n * n
    }

    return when(type) {
        "square" -> square(nn)
        "cube" -> cube(nn)
        else -> nn
    }
}
  1. 函数调用
// 局部函数
println(getMathFunc("square", 4)) //16
println(getMathFunc("cube", 3)) //27
println(getMathFunc("", 2)) //2
// 使用变量保存封闭函数返回的局部函数,这样局部函数就可以在其他作用域中使用
val squareMethod = { n: Int -> getMathFunc("square", n) }
println(squareMethod(6)) //36

高阶函数

Kotlin 中的函数也具有自己的类型,它既可用于定义变量,也可用作函数的形参类型,还可作为函数的返回值类型。

使用函数类型

组成方式:函数的形参列表、-> 和 返回值类型。

// 函数类型:(String, Int) -> String
fun higherOrderTest1(name: String, age: Int): String {
    return "$name 的年龄是:$age 岁"
}

// 函数类型:(Double, Double) -> Unit 或者 (Double, Double)
fun higherOrderTest2(d1: Double, d2: Double) {
}

// 函数类型:() -> Unit 或者 ()
fun higherOrderTest3() {
}
  1. 函数定义
fun pow(base: Int, exponent: Int): Int {
    var result = 1
    for (i in 1..exponent) {
        result *= base
    }
    return result
}

fun add(a: Int, b: Int): Int {
    return a + b
}
  1. 变量定义并赋值,调用
// 使用函数类型
// 定义一个函数类型的变量,其类型为 (Int, Int) -> Int
var myFun: (Int, Int) -> Int
// 将 pow 函数赋值给 myFun,则 myFun 可以当做 pow 使用
myFun = ::pow
println(myFun(3, 4)) //81
// 将 add 函数赋值给 myFun,则 myFun 可以当做 add 使用
myFun = ::add
println(myFun(3, 4)) //7
  1. 程序依次将pow()add()函数赋值给myFun变量:只要被赋值的函数类型与 myFun 的变量类型一致,就可赋值成功;
  2. ::pow:在函数名前添加两个冒号,这是函数引用,一旦加了括号,就变成了函数调用;
    :: 双冒号这种写法,官方称为 Function Reference,函数引用。但这个解释容易让人误解,其实这只是官方让大家方便上手才这么称呼的,真实的意思是:
    加个双冒号的函数,此时就变成了一个对象,没错,就是对象。
    在 Kotlin 里,「函数可以作为参数」这件事的本质,是函数可以作为对象存在,因为只有对象才可以被参数传递,赋值也是一样的道理。
    所以上面代码中 myFun 其实是个对象,但是对象是不能加括号来调用的,对吧?
    但是函数类型的对象可以,为什么?因为这是个假的调用,这是 Kotlin 的一个语法糖,实际执行的是这个对象的 invoke() 函数。
  3. 通过使用函数类型的对象,可以让 myFun 在不同时间指向不同对象的引用,从而让程序更加灵活。

使用函数类型作为函数形参

当某些程序代码需要动态改变,可通过传入不同函数作为参数,来动态改变这些代码。

  1. 函数定义
// 定义函数,其中 fn 是 (Int) -> Int 类型的形参
fun higherOrderTest4(data: Array<Int>, fn: (Int) -> Int): Array<Int> {
    // 定义返回值 result,所有元素都初始化为 0
    val result = Array(data.size) { 0 }
    // 遍历元素,并调用 fn 函数
    for (i in data.indices) {
        result[i] = fn(data[i])
    }
    return result
}

fun square(n: Int): Int {
    return n * n
}

fun cube(n: Int): Int {
    return n * n * n
}
  1. 函数调用
val data = arrayOf(3, 4, 9, 5, 8)
println("原数据 ${data.contentToString()}") //原数据 [3, 4, 9, 5, 8]
println("square: ${higherOrderTest4(data, ::square).contentToString()}") //square: [9, 16, 81, 25, 64]
println("cube: ${higherOrderTest4(data, ::cube).contentToString()}") //cube: [27, 64, 729, 125, 512]

使用函数类型作为返回值类型

Kotlin 还支持定义函数类型的返回值,这样即可将其他函数作为函数的返回值。

  1. 函数定义
// 使用函数类型的返回值
fun higherOrderTest5(type: String): (Int) -> Int {
    fun square(n: Int): Int {
        return n * n
    }

    fun cube(n: Int): Int {
        return n * n * n
    }
    
    fun other(n: Int): Int {
        return n
    }
    
    return when(type) {
        "square" -> ::square
        "cube" -> ::cube
        else -> ::other
    }
}
  1. 函数调用
// 使用函数类型的返回值
var test5 = higherOrderTest5("cube")
println(test5(2)) //8
test5 = higherOrderTest5("square")
println(test5(3)) //9
test5 = higherOrderTest5("minus")
println(test5(4)) //4

局部函数与 Lambda 表达式

如果说函数是命名的、方便复用的代码块,那么 Lambda 表达式则是功能更加灵活的代码块,它可以在程序中被传递和调用。

Lambda 表达式简化局部函数

由于局部函数的默认作用域有限,那么就可以考虑使用 Lambda 表达式来简化局部函数的写法。

  1. 函数定义
// 用 Lambda 替换 higherOrderTest5
fun higherOrderTest5Lambda(type: String): (Int) -> Int {
    return when (type) {
        "square" -> { n -> n * n }
        "cube" -> { n -> n * n * n }
        else -> { n -> n }
    }
}
  1. 函数调用
// 用 Lambda 替换局部函数 higherOrderTest5
var testLambda = higherOrderTest5Lambda("cube")
println(testLambda(2)) //8
testLambda = higherOrderTest5Lambda("square")
println(testLambda(3)) //9
testLambda = higherOrderTest5Lambda("minus")
println(testLambda(4)) //4

Lambda 表达式与局部函数的区别:

  1. Lambda 表达式总是被 { } 括着;
  2. 定义 Lambda 表达式不需要 fun 关键字,无须指定函数名;
  3. 形参列表(如果有的话)在 -> 之前声明,参数类型可以省略;
  4. 函数体放在 -> 之后;
  5. 函数的最后一个表达式自动被作为 Lambda 表达式的返回值,无须使用 return 关键字。

Lambda 表达式的脱离

作为函数参数传入的 Lambda 表达式可以脱离函数独立使用。

  1. 函数定义
// Lambda 表达式的脱离
// 定义一个 List ,用与存储 Lambda 表达式
val lambdaList = mutableListOf<(Int) -> Int>()
// 定义函数,存放 Lambda 表达式,这样 Lambda 表达式就可以脱离该函数使用
fun collectFn(fn: (Int) -> Int) {
    lambdaList.add(fn)
}
  1. 函数调用
// Lambda 表达式的脱离
collectFn({it * it})
collectFn { it * it * it }
println(lambdaList.size)
for (i in lambdaList.indices) {
    println(lambdaList[i](3)) //9 27
}

Lambda 表达式

Lambda 表达式也是函数类型的对象

Lambda 表达式的标准语法如下:

{(形参列表) -> 可执行语句}

调用 Lambda 表达式

Lambda 表达式的本质是功能更灵活的代码块,因此可直接赋值给变量或直接调用。

  1. 定义 Lambda 表达式
// 定义一个 Lambda 表达式
val squareLambda = { n: Int -> n * n }
// 定义一个 Lambda 表达式,并在它后面加上圆括号来直接调用
val powResultLambda = { base: Int, exponent: Int ->
    var result = 1
    for (i in 1..exponent) {
        result *= base
    }
    result
}(2, 3)
  1. 调用 Lambda 表达式
// Lambda 表达式的调用
println(squareLambda(4)) //16
println(powResultLambda) //8

利用上下文推断类型

如果 Kotlin 可以根据 Lambda 表达式上下文推断出形参类型,那么 Lambda 表达式就可以省略形参类型。

  1. 定义 Lambda 表达式
// 修改 squareLambda
val squareLambda2: (Int) -> Int = {n -> n * n}
// List 操作:由于 dropWhile 方法的形参是 (T) -> Boolean 类型,因此 Kotlin 可以推断出该 Lambda 表达式的形参类型就是集合元素的类型
val list = listOf("Java", "Kotlin", "Go")
val listResult = list.dropWhile { e -> e.length > 3 }
  1. 调用 Lambda 表达式
println(squareLambda2(4)) //16
println(listResult) //[Go]

省略形参名

Lambda 表达式不仅可以省略形参类型,而且如果只有一个形参,那么 Kotlin 允许省略 Lambda 表达式的形参名。如果 Lambda 表达式省略了形参名,那么 -> 也不需要了,并使用 it 来代表形参。
修改 squareLambda2listResult

// 修改 squareLambda
val squareLambda2: (Int) -> Int = { it * it }
// List 操作:由于 dropWhile 方法的形参是 (T) -> Boolean 类型,因此 Kotlin 可以推断出该 Lambda 表达式的形参类型就是集合元素的类型
val listResult = list.dropWhile { it.length > 3 }

调用 Lambda 表达式的约定

Kotlin 语言有一个约定

  1. 如果函数的最后一个参数是函数类型,而且你打算传入一个 Lambda 表达式作为相应的参数,那么就允许在圆括号之外指定 Lambda 表达式。也被成为“尾随闭包”。
  2. 如果 Lambda 表达式是函数调用的唯一参数,则圆括号也可以省略。
// 以下3中方式等价,建议使用第三种,更简洁
val listResult2 = list.dropWhile({ it.length > 3 })
val listResult2 = list.dropWhile() { it.length > 3 }
val listResult2 = list.dropWhile { it.length > 3 }

建议:通常建议将函数类型的形参放在形参列表最后,这样可以方便传入 Lambda 表达式。

个数可变的参数和 Lambda 参数

如果一个函数既包含个数可变的参数,也包含函数类型的参数,那么就将函数类型的参数放在最后,这样就可以使用 Lambda 表达式写在函数的圆括号外面了。

  1. 函数定义
fun varargLambdaTest(vararg str: String, reverseFn: (String) -> String): List<String> {
    val mutableList = mutableListOf<String>()
    for (i in str) {
        mutableList.add(reverseFn(i))
    }
    return mutableList
}
  1. 函数调用
val result = varargLambdaTest("123", "abc", "!@#") { it.reversed() }
println(result) //[321, cba, #@!]

匿名函数

匿名函数,不是函数,不是函数,不是函数,而是一个函数类型的变量
所以它才能作为函数的参数来传递,以及直接赋值给一个变量。
它与双冒号加函数名是一个东西,与函数不是

Lambda 表达式有个严重的缺陷,无法指定返回值类型,这时就可以使用匿名函数来代替。

匿名函数的用法

与普通函数基本相同,不同点:

  1. 去掉函数名;
  2. 如果系统可以推断出匿名函数的形参类型,那么匿名函数允许省略形参类型。
  1. 函数定义
// 定义匿名函数的函数体是单表达式,可以省略声明函数的返回值类型
val anonymousFunTest1 = fun(x: Int, y: Int): Int = x + y

// 定义匿名函数,来作为filter()的方法
val anonymousFunTest2 = listOf(3, 5, 20, 100, -25).filter(
    fun(el): Boolean {
        return el > 0
    }
)
  1. 函数调用
println(anonymousFunTest1(3, 4)) //7
println(anonymousFunTest2) //[3, 5, 20, 100]

匿名函数和 Lambda 表达式的 return

  1. 匿名函数中的 return 用于返回该函数本身;
  2. Lambda 中的 return 用于返回它所在的函数;
  3. Lambda 中的 return 使用限定返回的语法,可达到匿名函数 return 的效果。
fun main() {
    // 匿名函数中的 return 用于返回该函数本身,故不会终止循环
    val list = listOf(3, 5, 6, -3, 9)
    list.forEach(fun(n) {
        print("$n ")
        return
    })
    // 3 5 6 -3 9

    // Lambda 中的 return 用于返回它所在的函数(这里指的是 main()),故会终止循环
    list.forEach {
        print("$it ")
        return // 在这个 Lambda 表达式中能用 return,是因为forEach 使用了 inline 修饰
    }
    // 3

    // Lambda 中的 return 使用限定返回的语法,可达到匿名函数 return 的效果
    list.forEach {
        print("$it ")
        // 使用限定返回,此时 return 只是返回传给了 forEach 方法的 Lambda 表达式
        return@forEach
    }
    // 3 5 6 -3 9
}

捕获上下文中的变量和常量

Lambda 表达式或匿名函数可以访问或修改其所在上下文(“闭包”)中的变量,这就是捕获。
Lambda 表达式与匿名函数都会持有一个其所获取的变量的副本.

  1. 函数定义
// 定义一个函数,返回函数类型 () -> List<String>
fun makeList(ele: String): () -> List<String> {
    // 定义一个 list
    val list = mutableListOf<String>()
    // 定义一个局部函数,返回 list
    // 注意:该局部函数没有任何参数也没有任何变量,但是可以访问到其所在函数的参数与变量,这就是捕获
    fun addElement(): List<String> {
        list.add(ele)
        return list
    }
    // 返回局部函数
    return ::addElement
}
  1. 函数调用
// 注意:Lambda 表达式与匿名函数都会持有一个其所获取的变量的副本
// add1() 拥有 makeList 函数中 list 的副本
val add1 = makeList("孙悟空")
println(add1()) //[孙悟空]
println(add1()) //[孙悟空, 孙悟空]
// add2() 也拥有 makeList 函数中 list 的副本,与 add1() 的 list 无关
val add2 = makeList("猪八戒")
println(add2()) //[猪八戒]
println(add2()) //[猪八戒, 猪八戒]

内联函数

以下所有内容均来自凯哥分享的内容,自己实践后的学习笔记。
传送门:Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?看完这个视频你转头也写了一吨

inlinenoinlinecrossinline
Kotlin 里被 inline 修饰的函数,叫做内联函数。
所谓的内联就是,调用的函数在编译的时候会变成代码内嵌的形式。

inline 关键字

普通函数声明调用及其调用栈

// 声明普通函数
fun hello() {
    println("Hello")
}
// 调用处
fun main() {
    hello()
}
// 实际编译的代码
fun main() {
    hello()
}
// 调用栈
// println()
// hello()
// main()

内联函数声明调用及其调用栈

// 声明内联函数
inline fun hello() {
    println("Hello")
}
// 调用处
fun main() {
    hello()
}
// 实际编译的代码
fun main() {
    println("Hello")
}
// 调用栈
// println()
// main()

优点:调用栈变浅了,减少了时间和空间上的开销(事实上,这种优化效果非常小,小到了应该被忽略的程度
缺点:可能会因为代码的多处拷贝,导致编译生成的字节码膨胀,从而变成了负优化。

既然优化效果非常小,那 inline 还有什么用呢?

举个栗子:
我有个函数,传入一个函数类型的参数

fun hello(action: () -> Unit) {
    println("Hello")
    action() // do something...
}

然后在其他地方被调用

fun main() {
    hello { // 每次调用 hello 都会创建一个对象
        println("Bye")
    }
}

实际编译的代码(大致)

fun main() {
    // 上述代码实际编译后的代码(大致)
    val action = object : Function0<Unit> {
        override fun invoke() {
            return println("Bye")
        }
    }
    hello(action)
}

由于每次调用 hello 都会创建出一个新对象,那如果 hello 被用在循环中呢?内存一下就上来了,就有了性能隐患,这时 inline 就出场了。
inline 不仅会把当前的函数内联过来,还会把它内部的函数类型的参数(那些 Lambda 表达式也一一内联过来)

修改 hello 函数,加上 inline 关键字修饰

inline fun hello(action: () -> Unit) {
    println("Hello")
    action() // do something...
}

调用方式不变,实际编译的代码如下

fun main() {
    println("Hello")
    println("Bye")
}

发现了吗?代码被完全铺平了,也不会创建临时对象了,就算在循环中使用,也不会有性能隐患了。赞!赞!赞!
这就是 inline 关键字的用处:高阶函数有它们天然的性能缺陷,我们可以通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题。

那什么时候需要加上 inline 这个关键字呢?
如果你写的是高阶函数,那么就加上 inline 关键字;
如果对包大小有极致的追求,那么就酌情使用 inline 关键字,只有可能被频繁调用的高阶函数(比如工具方法)才用 inline 关键字。

noinline 关键字

noinline 的意思很直白,就是不内联。不过它不是作用于函数的,而是作用于函数的参数:对于一个标记了 inline 的内联函数,你可以对它的任何一个或多个函数类型的参数添加 noinline 关键字。

函数定义

inline fun hello(preAction: () -> Unit, noinline postAction: () -> Unit) {
    preAction() // 做点前置工作
    println("Hello")
    postAction() // 做点后续工作
}

函数调用

fun main() {
    hello({
        println("pre...")
    }, {
        println("post...")
    })
}

实际编译的代码(大致)

fun main() {
    // 上述代码实际编译后的代码(大致)
    println("pre...")
    println("Hello")
    ({
        println("post...")
    }).invoke()
}

但是,这又什么用呢?为什么要关闭这种优化?
我们再修改下 hello 函数(首先,我们要知道,函数类型的参数本质上是个对象,那就可以被当做返回值来使用)

inline fun hello(preAction: () -> Unit, postAction: () -> Unit): () -> Unit {
    preAction() // 做点前置工作
    println("Hello")
    postAction() // 做点后续工作
    return postAction
}

上面代码编译器其实会报错,我们暂时先不管,后面再调整
那现在假设上面方法可以被正常调用(调用方式不变),那实际编译的代码如下

fun main() {
    println("pre...")
    println("Hello")
    println("post...")
    postAction
}

postAction ???这是啥?当一个函数被内联之后,它内部的那些参数就不再是对象了,因为他们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?
所以,上面的 hello 方法编译器就会报错,那怎么改呢?
编译器就会提示你,给这个参数加上 noinline 修饰就好了,不让这个高阶函数参与内联。
再次修改 hello 函数

inline fun hello(preAction: () -> Unit, noinline postAction: () -> Unit): () -> Unit {
    preAction() // 做点前置工作
    println("Hello")
    postAction() // 做点后续工作
    return postAction
}

这时,实际编译编译的代码如下

fun main() {
    println("pre...")
    println("Hello")
    val postAction = ({
        println("post...")
    }).invoke()
    postAction
}

此时postAction 这个对象就存在,也就可以正常调用了。
这就是 noinline 关键字的用处:用来局部的、指向性的关闭函数的内联优化。因为这种优化可能会导致函数中的函数类型参数无法被当做对象使用。

那么,什么时候加上 noinline 这个关键字呢?
比 inline 还简单,你不需要自己判断,Android Studio 会告诉你的。当你在内联函数里对函数类型的参数使用了风骚操作,Android Studio 拒绝编译的时候,你再加上 noinline 就可以了。

corssinline 关键字

刚刚说的 noinline 关键字是局部关闭内联优化,而这个 crossinline 是局部加强内联优化。
先看代码,还是上面的 hello

inline fun hello(postAction: () -> Unit) {
    println("Hello")
    postAction()
}

调用 hello,并在 Lambda 表达式中添加 return

fun main() {
    hello {
        println("Bye")
        return // 这里会 return main()
    }
}

return 为什么结束的不是 hello ,而是结束的 main
因为,hello 是内联函数,我们看下上面代码实际编译后的代码

fun main() {
    // 上面代码实际编译后的代码
    println("Hello")
    println("Bye")
    return
}

由此可见,return 的是 main 方法。
Kotlin 还为 Lambda 中的 return 制定了一条规则:
Lambda 表达式里不允许使用 return,除非这个 Lambda 是内联函数的参数

其实也就是说:

  1. Lambda 里的 return 结束的不是直接的外层函数,而是外层再外层的函数;
  2. 只有内联函数的 Lambda 参数可以使用 return

接下来,我们再把情况变得复杂点,修改下 hello 的代码(以下代码也会报错,后面修改)

inline fun hello(postAction: () -> Unit) {
    println("Hello")
    thread {
        postAction()
    }
}

我们把 postAction 放在了子线程中执行,这样 hello 对它的调用就变成了间接调用,直白点说就是它和外层 hello 函数的关系被切换断了,那就更加够不着更外层的 main 了,所以这样 return 就无法结束 main 了。
那怎么解决这个问题呢?
编译器会告诉你,加上 crossinline 就好了。hello 函数再次被修改

inline fun hello(crossinline postAction: () -> Unit) {
    println("Hello")
    thread {
        postAction()
    }
}

这就是 crossinline 这个关键字的用处:给一个需要被间接调用的参数加上 crossinline ,就对它解除了这个限制,从而就可以对它进行间接调用了。

现在解决了 hello 函数的问题,那我在调用处的 Lambda 表达式里加入 return 后,又会怎么样呢?能否正常结束 main 方法呢?
显然还是无法解决上面的问题,所以 Kotlin 又增加了一条额外的规则:
内联函数里被 crossinline 修饰的函数类型的参数,将不再拥有 「Lambda 表达式可以使用 return」 的福利。所以这个 return 并不会面临 「要结束谁」 的问题,而直接不允许这么写,编译器会直接拒绝编译。
即:间接调用 和 Lambda 的 return,你只能选一个

那么,什么时候加上 crossinline 这个关键字呢?
当你要突破内联函数 "不能间接调用" 的限制的时候。
其实和 noinline 一样,Android Studio 会提示报错,这时候你加上就好了。

inline、noinline、crossinline总结

  1. inline:可以让你用内联(也就是函数内容直插到调用处)的方式来优化代码结构,从而减少函数类型的对象的创建;
  2. noinline:局部关掉这个优化,来摆脱 inline 带来的「不能把函数类型的参数当对象使用」的限制;
  3. crossinline:局部加强这个优化,来突破内联函数「不能间接调用」的限制。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345