第一行代码学习补漏

222 阅读23分钟

类的继承

kotlin默认所有非抽象类都是不可以被继承的,相当于Java中给类声明了final关键字。之所以这么设计,其实和val关键字的原因是差不多的,因为类和变量一样,最好都是不可变的,而一个类允许被继承的话,它无法预知子类会 如何实现,因此可能就会存在一些未知的风险。

如果想要某个类能被继承需两步操作

  • 在父类前面open关键字
open class Person(){

}
  • Java中继承的关键字是extends,而在Kotlin中变成了一个冒号
class Student : Person() {
    var sno = ""
    var grade = 0 
}

主构造函数与次构造函数

继承的写法如果只是替换一下关键字倒也挺简单的,但是为什么Person类的后面要加上一对括号呢?这里涉及到主构造函数与次构造函数概念。

  • 主构造函数

主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数 的主构造函数,当然你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可。比如下面这种写法:

class Student(val sno: String, val grade: Int) : Person() {
}

//实例化:必须传入构造函数中要求的所有参数
val student = Student("a123", 5)

另外,由于构造函数中的参数是在创建实例的时候传入的,不像之前的写法那样还得重新赋值,因此我们可以将参数全部声明成val

到这里为止都还挺好理解的吧?但是这和那对括号又有什么关系呢?这就涉及了Java继承特性中的一个规定,子类中的构造函数必须调用父类中的构造函数,这个规定在Kotlin中也要遵守。

那么回头看一下Student类,现在我们声明了一个主构造函数,根据继承特性的规定,子类的构造函数必须调用父类的构造函数,可是主构造函数并没有函数体,我们怎样去调用父类的构造函数呢?

括号。子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。 因此再来看一遍这段代码,你应该就能理解了吧。

class Student(val sno: String, val grade: Int) : Person() {
}

在这里,Person类后面的一对空括号表示Student类的主构造函数在初始 化的时候会调用Person类的无参数构造函数,即使在无参数的情况下,这对括号也不能省略。

而如果我们将Person改造一下,将姓名和年龄都放到主构造函数当中,如下所示:

open class Person(val name: String, val age: Int) { ...

}

此时Stuent类会报错,如果我们想解决这个错误的话,就必须给Person类的构造函数传入name 和age字段,可是Student类中也没有这两个字段呀。很简单,没有就加呗。我们可以在Student类的主构造函数中加上name和age这两个参数,再将这两个参数传给Person类的构造函数,代码如下所示:

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {

}

注意,我们在Student类的主构造函数中增加name和age这两个字段时, 不能再将它们声明成val,因为在主构造函数中声明成val或者var的参数 将自动成为该类的字段,这就会导致和父类中同名的name和age字段造成 冲突。

  • 次构造函数

你要知道,任何一个类只能有一个主构造函数,但是可以有多个次构造函 数。次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么 不同,只不过它是有函数体的。

Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。这里我通过一个具体的例 子就能简单阐明,代码如下

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {

    //Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造 函数都必须调用主构造函数(包括间接调用)。这里我通过一个具体的例 子就能简单阐明,代码如下:
    constructor(name: String, age: Int) : this("", 0, name, age)
    constructor() : this("", 0)
}

次构造函数是通过constructor关键字来定义的,这里我们定义了两个次 构造函数:第一个次构造函数接收name和age参数,然后它又通过this关键字调用了主构造函数,并将sno和grade这两个参数赋值成初始值;第二 个次构造函数不接收任何参数,它通过this关键字调用了我们刚才定义的 第一个次构造函数,并将name和age参数也赋值成初始值,由于第二个次 构造函数间接调用了主构造函数,因此这仍然是合法的。

那么现在我们就拥有了3种方式来对Student类进行实体化

val student1 = Student()
val student2 = Student("Jack", 19)
val student3 = Student("a123", 5, "Jack", 19)

那么接下来我们就再来看一种非常特殊的情况:类中只有次构造函数,没有主构造函数。这种情况真的十分少见,但在Kotlin中是允许的。当一个类没有显式地定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。我们结合代码来看一下:


class Student : Person {
    constructor(name: String, age: Int) : super(name, age) {
    }
}

现在Student类是没有主构造函 的。那么既然没有主构造函数,继承Person类的时候也就不需要再加上 括号了。另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数, 上述代码也是将this关键字换成了super关键字,这部分就很好理解了, 因为和Java比较像,我也就不再多说了。

接口

kotlin允许对接口中定义的函数进行默认实现。其实Java在JDK 1.8之后也开始支持这个功能 了,因此总体来说,Kotlin和Java在接口方面的功能仍然是一模一样的。


interface Study {
    fun readBooks()
    fun doHomework() {
        println("do homework default implementation.")
    }
}

现在当一个类去实现Study接口时,只会强制要求实现 readBooks()函数,而doHomework()函数则可以自由选择实现或者不实 现,不实现时就会自动使用默认的实现逻辑。

函数的可见性修饰符

  • Java Java中有public、private、protected和 default(什么都不写)这4种函数可见性修饰符

  • Kotlin Kotlin中也有4种,分别是public、private、protected和internal,

修饰符JavaKotlin
public所有类可见所有类可见(默认)
private当前类可见当前类可见
protected当前类、子类、同一包路径下的类可见当前类、子类可见
default同一包路径下的类可见(默认)
internal同一模块中的类可见

数据类

  • 数据类会根据主构造函数中的参数帮你将equals()、 hashCode()、toString()等固定且无实际逻辑意义的方法自动生成, 从而大大减少了开发的工作量。
  • 数据类必须有主构造函数
  • 关键字 data
data class CellPhone(val brand:String,val price:Double) {

}

//
val cellphone1 = CellPhone("Samsung", 1299.99)
val cellphone2 = CellPhone("Samsung", 1299.99)
println(cellphone1)
println("cellphone1 equals cellphone2 " + (cellphone1 == cellphone2))

//日志会打印两个比较为true

单利类&静态类

在Kotlin中创建一个单例类的方式极其简单,只需要将class关键字改成 object关键字即可。 第一行代码——Android(第3版)

object Singleton {
    fun singletonTest() {
        println("singletonTest is called.") 
    }
}

可以看到,在Kotlin中我们不需要私有化构造函数,也不需要提供 getInstance()这样的静态方法,只需要把class关键字改成object关 键字,一个单例类就创建完成了。而调用单例类中的函数也很简单,比较 类似于Java中静态方法的调用方式

    Singleton.singletonTest()

Lambda编程

  • 定义 Lambda就 是一小段可以作为参数传递的代码。因为正常情况下,我们向某个函数传参时只能传入变量,而借助Lambda却允许传入一小段代码

  • 语法结构 {参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}

  • Lambda表达式推算过程

//场景一:如何在一个水果集合里面找到单词最长的那个 水果?
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val maxlengthFruit = list.maxBy { it.length }
//约等于
val lambda = { fruit: String -> fruit.length }
val maxlengthFruit2 = list.maxBy(lambda)
//简化操作1:
val maxlengthFruit3 = list.maxBy({ fruit: String -> fruit.length })
//简化操作2:
val maxlengthFruit4 = list.maxBy() { fruit: String -> fruit.length }
//简化操作3:
val maxlengthFruit5 = list.maxBy { fruit: String -> fruit.length }
//简化操作4:(类型推到机制)
//由于 Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多 数情况下不必声明参数类型,因此代码可以进一步简化成:
val maxlengthFruit6 = list.maxBy { fruit -> fruit.length }
//简化操作5:当Lambda表达式的参数列表中只有一个参数时,也不必声明参数 名,而是可以使用it关键字来代替
val maxlengthFruit7 = list.maxBy { it.length }

集合中比较常用的函数式API

  • API 1:map函数

它用于将集合中的每个元素 都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一 个新的集合

//比如,这里我们希望让所有的水果名都变成大写模式,就可 以这样写:
val newList = list.map { it.toUpperCase() }
for (item in newList) {
    println("Result is " + item)
}
  • API 2:filter函数 顾名思义,filter函数是用来过滤集合中的数据的,它可以单独使用,也可 以配合刚才的map函数一起使用
//比如我们只想保留5个字母以内的水果,就可以借助filter函数来实现,
val newList2 = list.filter { it.length <= 5 }.map { it.toUpperCase() }
for (item in newList2) {
    println("Result is " + item)
}
  • API 3:any函数

any函数用于判断集合中是否至少存在一个元素满足指定条件

val anyResult = list.any { it.length <= 5 }
  • API 4:all函数 all函数用 于判断集合中是否所有元素都满足指定条件
val allResult = list.all { it.length >= 5 }
println("anyResult is " + anyResult + ", allResult is " + allResult)

标准函数let、with、 run 和 apply

Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码 都可以自由地调用所有的标准函数。

  • let 它的主要作用就是配 合?.操作符来进行辅助判空处理
  • with with函数接收两个参数:第一个参数可以 是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在 Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的 最后一行代码作为返回值返回。
val result = with(obj) {
    // 这里是obj的上下文 
    "value" // with函数的返回值
}

作用:它可以在连续调用同一个对象的多个方法时 让代码变得更加精简。 例如:

val listFruits = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val builder=StringBuilder()
builder.append("Start eating fruits.\n")
for (fruit in listFruits) {
    builder.append(fruit).append("\n")
}
builder.append("Eat all fruits")
var results=builder.toString()
print(results)

//利用with函数
val  results2= with(StringBuilder()){
    append("Start eating fruits.\n")
    for (fruit in listFruits) {
        builder.append(fruit).append("\n")
    }
    append("Eat all fruits")
    toString()
}
  • run函数 run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已,run函数通常不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。其他方面和with函数是一样的,包括也会使用Lambda表达式中的最后一行代码作为返回值返回。
val result = obj.run {
    // 这里是obj的上下文 
    "value" // run函数的返回值
}

例如:

val listFruits = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val builder=StringBuilder()
builder.append("Start eating fruits.\n")
for (fruit in listFruits) {
    builder.append(fruit).append("\n")
}
builder.append("Eat all fruits")
var results=builder.toString()
print(results)

//利用run函数
val  results3= StringBuilder().run{
    append("Start eating fruits.\n")
    for (fruit in listFruits) {
        builder.append(fruit).append("\n")
    }
    append("Eat all fruits")
    toString()
}
  • apply函数 apply函数和run函数也是极 其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在 Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回 值,而是会自动返回调用对象本身。
val result = obj.apply { // 这里是obj的上下文

}
// result == obj

例如:

val results4=StringBuilder().apply {
    append("Start eating fruits.\n")
    for (fruit in listFruits) {
        builder.append(fruit).append("\n")
    }
    append("Eat all fruits")
}
print(results4.toString())

常见的定义静态方法形式

  • 单利模式
object Singleton {
    fun singletonTest() {
        println("singletonTest is called.") 
    }
}
  • 伴生类
class Util {

    fun doAction1() {
        print("do action1")
    }

    companion object {
        //可以直接使用Util.doAction2()的方式调用
        //注意,@JvmStatic注解只能加在单例类或companion object中的方法 上,如果你尝试加在一个普通方法上,会直接提示语法错误。
        //由于doAction2()方法已经成为了真正的静态方法,那么现在不管是在 Kotlin中还是在Java中,都可以使用Util.doAction2()的写法来调用了
        @JvmStatic
        fun doAction2() {
            print("do action2")
        }
    }
}
  • 将方法定义在xxx.kt顶层文件中

延时初始化

代码中有了越来越多的全局变量实例时,你可能必须编写大量额外的判空处理代码,只是为了满足 Kotlin编译器的要求。解决方案:延迟初始化。使用lateinit关键字,它可以告诉Kotlin编译器,我会在 晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为 null了。

class MainActivity : AppCompatActivity(), View.OnClickListener {
    private lateinit var adapter: MsgAdapter

    override fun onCreate(savedInstanceState: Bundle?) { 
        ...
        adapter = MsgAdapter(msgList) 
        ...

    }

    override fun onClick(v: View?) { 
        ...
        adapter.notifyItemInserted(msgList.size - 1) 
        ...

    } 
}

当然,使用lateinit关键字也不是没有任何风险,如果我们在adapter 变量还没有初始化的情况下就直接使用它,那么程序就一定会崩溃

另外,我们还可以通过代码来判断一个全局变量是否已经完成了初始化, 这样在某些时候能够有效地避免重复对某一个变量进行初始化操作,示例 代码如下: 第一行代码——Android(第3版)

class MainActivity : AppCompatActivity(), View.OnClickListener {

    private lateinit var adapter: MsgAdapter

    override fun onCreate(savedInstanceState: Bundle?) { 
        ...

        if (!::adapter.isInitialized) { //固定写法
            adapter = MsgAdapter(msgList)

        } 
        ...

    } 
}

扩展函数

  • 定义: 扩展函数表示即使在不修改某个类的源码的 情况下,仍然可以打开这个类,向该类添加新的函数。

  • 语法结构

fun ClassName.methodName(param1: Int, param2: Int): Int { 
    return 0

}

相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。

  • 举例 统计一段字符串中可能包含 字母、数字和特殊符号等字符,现在我们希望统计字符串中字母的数量, 你要怎么实现这个功能呢?如果按照一般的编程思维,可能大多数人会很自然地写出如下函数:
object StringUtil {
    fun lettersCount(str: String): Int { 
        var count = 0
        for (char in str) {
            if (char.isLetter()) {
                count++ 
            }
        }
     return count
  }

}

当我们需要统计某个字符串中的字母数量时,只需要编写如下代码 即可:

val str = "ABC123xyz!@#"
val count = StringUtil.lettersCount(str)

我们希望向String类中添加一个扩展函数,因此需要先创建一个 String.kt文件。 并编写如下代码:


fun String.lettersCount(): Int { 
    var count = 0
    for (char in this) {
        if (char.isLetter()) {
            count++ 
        }
    }
    return count
}

定义好了扩展函数之后,统计某个字符串中的字母数量只需要这样写即可:

val count = "ABC123xyz!@#".lettersCount()

运算符重载

运算符重载是Kotlin提供的一个比较有趣的语法糖。我们知道,Java中有 许多语言内置的运算符关键字,如+ - * / % ++ --。而Kotlin允许我 们将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关 键字的用法。

我们先来回顾一下运算符的基本用法。相信每个人都使用过加减乘除这种 四则运算符。在编程语言里面,两个数字相加表示求这两个数字之和,两 个字符串相加表示对这两个字符串进行拼接,这种基本用法相信接触过编程的人都明白。但是Kotlin的运算符重载却允许我们让任意两个对象进行相加,或者是进行更多其他的运算操作。

运算符重载使用的是operator关键字,只要在指定函数的前面加上 operator关键字,就可以实现运算符重载的功能了。但问题在于这个指定 函数是什么?这是运算符重载里面比较复杂的一个问题,因为不同的运算 符对应的重载函数也是不同的。比如说加号运算符对应的是plus()函数, 减号运算符对应的是minus()函数。

我们这里还是以加号运算符为例,如果想要实现让两个对象相加的功能, 那么它的语法结构如下:

class Obj {
    operator fun plus(obj: Obj): Obj { 
        // 处理相加的逻辑
    } 
}

关键字operator和函数名plus都是固定不变的,而 接收的参数和函数返回值可以根据你的逻辑自行设定

下面我们开始实现一个更加有意义功 能:让两个Money对象相加。

class Money(var value: Int) {
    operator fun plus(money: Money): Money {
        val sum = value + money.value
        return Money(sum)
    }

    operator fun plus(newValue: Int): Money {
        val sum = value + newValue
        return Money(sum)
    }
}

执行代码:

val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2 
println(money3.value)

//Money对象能够直接和数字相加
val money4 = money3 + 20 
println(money4.value)

语法糖表表达式和实际调用函数对照表

语法糖表表达实际调用函数
a+ba.plus(b)
a-ba.minus(b)
a*ba.times(b)
a/ba.div(b)
a%ba.rem(b)
a++a.inc()
a--a.dec()
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a==ba.equals(b)
a>ba.equals(b)
a<ba.equals(b)
a>=ba.equals(b)
a<=ba.compareTo(b)
a..ba.rangeTo(b)
a[b]a.get(b)
a[b]=ca.set(b,c)
a in ba.contains(b)

高阶函数

如果一个函数接收另一个函数作为参数, 或者返回值的类型是另一个函数,那么该函数就称为高阶函数。

  • 函数类型语法规则定义如下:
(String, Int) -> Unit

既然是定义一个函数类型,那么最关键的就是要声明该函数接收什么参 数,以及它的返回值是什么。因此,->左边的部分就是用来声明该函数接 收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一 对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类 型,如果没有返回值就使用Unit,它大致相当于Java中的void。

例如:

fun example(func: (String, Int) -> Unit) { 
    func("hello", 123)
}

可以看到,这里的example()函数接收了一个函数类型的参数,因此 example()函数就是一个高阶函数。而调用一个函数类型的参数,它的语法类似于调用一个普通的函数,只需要在参数名的后面加上一对括号,并在括号中传入必要的参数即可。

  • 作用: 那就是高阶函数允许让函数类型的参数来决定函数的执行逻辑。即 使是同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑 和最终的返回结果就可能是完全不同的

  • 实例1: 定义一个叫作num1AndNum2()的高阶函数,并让它接收两个整 型和一个函数类型的参数。

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

在num1AndNum2()函数中,我们没有进行任何具体的运算操作,而是将num1和num2参数传给了第三个函数类型参数,并获取它的返回值,最终 将得到的返回值返回。

如何调用它呢?

由于 num1AndNum2()函数接收一个函数类型的参数,因此我们还得先定义与其函数类型相匹配的函数才行。

fun plus(num1: Int, num2: Int): Int {
    return num1 + num2
}

fun minus(num1: Int, num2: Int): Int { 
    return num1 - num2
}

现在我们就可以调用num1AndNum2()函数了

//注意这里调用num1AndNum2()函数的方式,第三个参数使用了::plus 和::minus这种写法。
//这是一种函数引用方式的写法,表示将plus()和 minus()函数作为参数传递给num1AndNum2()函数
fun main() {
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1, num2, ::plus) 
    val result2 = num1AndNum2(num1, num2, ::minus) 
    println("result1 is $result1") 
    println("result2 is $result2")
}

//运行结果:
result1 is 180
result2 is 20

使用这种函数引用的写法虽然能够正常工作,但是如果每次调用任何高阶 函数的时候都还得先定义一个与其函数类型参数相匹配的函数,所以推荐使用Lambda表达式调用高阶函数

fun main() {
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1, num2) { n1, n2 ->
        n1 + n2 
    }

    val result2 = num1AndNum2(num1, num2) { n1, n2 -> 
        n1 - n2
    }
    println("result1 is $result1") 
    println("result2 is $result2")
}
  • 实例2: 使用高阶函数模仿实 现一个StringBuilder功能。
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {

    block()
    return this 
}

这里我们给StringBuilder类定义了一个build扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是StringBuilder。

注意,这个函数类型参数的声明方式和我们前面学习的语法有所不同:它在函数类型的前面加上了一个StringBuilder. 的语法结构。这是什么意思呢?其实这才是定义高阶函数完整的语法规则,在函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个类当中的。

那么这里将函数类型定义到StringBuilder类当中有什么好处呢?好处就 是当我们调用build函数时传入的Lambda表达式将会自动拥有 StringBuilder的上下文,同时这也是apply函数的实现方式。

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") 
    val result = StringBuilder().build {
        append("Start eating fruits.\n") 
        for (fruit in list) {
            append(fruit).append("\n") 
        }
        append("Ate all fruits.") 
    }

    println(result.toString()) 
}

内联函数

  • 内联函数使用的原因 因为Kotlin高阶函数背后的实现原理,会将我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式。 例如:
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

fun main() {
    val num1 = 100
    val num2 = 80
    val result = num1AndNum2(num1, num2) { n1, n2 ->
        n1 + n2 
    }

}

可是我们都知道,Kotlin的代码最终还 是要编译成Java字节码的,但Java中并没有高阶函数的概念。Kotlin的编译器会将这些高阶函数的语法转换成Java支持的语法结构,上述的Kotlin代码大致会被转换成如下Java代码:

public static int num1AndNum2(int num1, int num2, Function operation) { 
    int result = (int) operation.invoke(num1, num2);
    return result;
}

public static void main() { 
    int num1 = 100;
    int num2 = 80;
    int result = num1AndNum2(num1, num2, new Function() {
        @Override
        public Integer invoke(Integer n1, Integer n2) {
           return n1 + n2;
        }
    }); 
}

考虑到可读性,我对这段代码进行了些许调整,并不是严格对应了Kotlin 转换成的Java代码。可以看到,在这里num1AndNum2()函数的第三个参数 变成了一个Function接口,这是一种Kotlin内置的接口,里面有一个待实 现的invoke()函数。而num1AndNum2()函数其实就是调用了Function 接口的invoke()函数,并把num1和num2参数传了进去。

在调用num1AndNum2()函数的时候,之前的Lambda表达式在这里变成了 Function接口的匿名类实现,然后在invoke()函数中实现了n1 + n2的 逻辑,并将结果返回。

这就是Kotlin高阶函数背后的实现原理。你会发现,原来我们一直使用的 Lambda表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调 用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外 的内存和性能开销。

为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda 表达式带来的运行时开销完全消除。

  • 内联函数的定义:
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
   val result = operation(num1, num2)
   return result
}
  • 内联函数的原理 就是Kotlin编译器 会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。

1:Kotlin编译器会将Lambda表达式中的代码替换到函数类型参数调用的地方 第一步替换.png

2: 将内联函数中的全部代码替换到函数调用的地方

第二步替换.png

3:最终的代码就被替换成了

第三步替换.png

noinline与crossinline

接下来我们要讨论一些更加特殊的情况。比如,一个高阶函数中如果接收 了两个或者更多函数类型的参数,这时我们给函数加上了inline关键字,那么Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。 但是,如果我们只想内联其中的一个Lambda表达式该怎么办呢?这时就可 以使用noinline关键字了,如下所示:

inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}

可以看到,这里使用inline关键字声明了inlineTest()函数,原本 block1和block2这两个函数类型参数所引用的Lambda表达式都会被内 联。但是我们在block2参数的前面又加上了一个noinline关键字,那么 现在就只会对block1参数所引用的Lambda表达式进行内联了。这就是 noinline关键字的作用。

内联函数类与非内联函数的区别

  • 因为内联的函数类型参数在编译 的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类 型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而 内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局 限性。
  • 内联函数和非内联函数还有一个重要的区别,那就是内联函数所引 用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回

例如:

  • 局部返回
fun printString(str: String, block: (String) -> Unit) { 
    println("printString begin")
    block(str)
    println("printString end")
}
fun main() {
    println("main start") 
    val str = "" 
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty())return@printString 
        println(s)
        println("lambda end")
    }
    println("main end")
}

//运行结果:
main start
printString begin
lambda start
printaString end
main end

注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了 return@printString的写法,表示进行局部返回,并且不再执行Lambda 表达式的剩余部分代码。

  • 非局部返回
inline fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
    block(str)
    println("printString end")

}

fun main() {
    println("main start") 
    val str = "" 
    printString(str) { s ->
        println("lambda start") 
        if (s.isEmpty())
        return println(s) 
        println("lambda end")
    }
    println("main end") 
}

//运行结果:
main start
printString begin
lambda start

现在printString()函数变成了内联函数,我们就可以在Lambda表达式中 使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学 习的内联函数的代码替换过程。