Kotlin 学习笔记(三)

2022-08-19 16:48:56 浏览数 (1)

最近发生太多事,从吴某凡的瓜开始,到杭州市父女俩骑电动车起火,再到郑州暴雨,还有2020年东京奥运会,记者们估计都忙疯了吧。。。还有在线教育 K12 的毁灭性打击,疫情又开始反复,哎~ 这个暑假真的是太多事儿了,这是不是在教育我们,世事难料,要及时行乐?不管咋样,还是得脚踏实地,提升自我,只有这样才有余力去帮助他人。

在上一篇中我们见识到了 Kotlin 语言所特有的几种类——数据类、密闭类等,也熟悉了 Kotlin 中集合的常用运算符,以后再也不用担心 Kotlin 集合的相关问题了。这是笔记系列的第三篇,渐入佳境了吧!

1. Kotlin 作用域函数

如果同学们已经在项目中用过 Kotlin 语言,那么一定见过 let 函数!因为每当 Kotlin 检测到某个对象可能为空时,会自动帮我们修改为用 let 函数实现:user.name?.let{ textView.text = it }。这里的 let 函数就是 Kotlin 的作用域函数。除了 let,还有 run、with、apply、also 等等作用域函数。

作用域函数是 Kotlin 内置的,可对数据做一系列操作、变换的函数,与集合操作符类似,作用域函数不仅仅可被集合对象调用,它们还可以被所有对象调用。让我们来看看它们的用法。

let 和 run 函数类似,都会返回函数内闭包的结果,区别在于 let 有闭包参数,而 run 没有闭包参数。使用方法:let{ 闭包 }、run{ 闭包 },有闭包参数意思是 let 在闭包中可以通过 it 拿到它自己本身;而 run 就不行了,只能通过 this 关键字拿到它本身。看 code 1 例子。

代码语言:javascript复制
// code 1
data class Car(
    val brand: String,
    val price: Int
)

var car: Car? = null
fun funcExample() {
    car = Car("红旗", 199999)
    // let 闭包里可用 it 访问调用者,可返回闭包的执行结果
    val carBrand = car?.let { "car's brand is ${it.brand}" }
    println(carBrand)
    // run 闭包用 this 访问调用者
    val carPrice = car?.run { "car's price is ${this.price}" }
    println(carPrice)
}

also和 apply 函数不会返回闭包里的结果,而上述的 let 和 run 是可以返回闭包结果的。also和 apply 函数的区别也是在于有无闭包参数:also 有闭包参数,apply 没有闭包参数。但是它们都会返回调用者对象,所以它们支持链式调用。

代码语言:javascript复制
// code 2
// also 闭包里可用 it 访问调用者,后面可链式调用
car?.also {
    println("car 贴牌前 brand = ${it.brand}")
}?.brand = "比亚迪"
// apply 闭包用 this 访问调用者,后面也可链式调用
car?.apply {
    println("car 贴牌后 brand = ${this.brand}")
}?.apply {
    println("car's price = ${this.price}")
}

takeIf 和 takeUnless 这两个作用域函数就用的相对较少了。takeIf 函数里的闭包返回的是 Boolean 类型,如果闭包条件满足,则返回调用者本身,如果不满足,则返回 null。举个栗子来说明吧。

代码语言:javascript复制
// code 3
car?.takeIf { it.price > 1500000 }
    ?.also { println("车太贵啦!") }    // 闭包为 true,则不为空,执行 also 函数闭包
    ?: run { println("价格还行!") }    // 闭包为 false,则返回空,执行 run 函数闭包

takeUnless 跟 takeIf 是相反的关系,takeUnless 的闭包条件满足则返回空,不满足则返回调用者自己。

repeat 函数。调用方法:repeat( times ) { 闭包 }。将闭包的操作执行 times 次。闭包里面的 it 是当前执行的循环次数,从 0 开始计数。

代码语言:javascript复制
// code 4
repeat(3) {
        println("car' brand is ${car?.brand}, price is ${car?.price} 当前执行次数为:$it")
}

执行结果:
car' brand is 比亚迪, price is 199999 当前执行索引为:0
car' brand is 比亚迪, price is 199999 当前执行索引为:1
car' brand is 比亚迪, price is 199999 当前执行索引为:2

with 函数。调用方法:with( T ){ 闭包 }。就是将对象 T 去执行闭包里的操作,通常在 Android 开发中,需要对一个 TextView 赋值时,就可以使用 with,比较方便:

代码语言:javascript复制
// code 5
with(textView) {
    text = "测试"
    textSize = 20F
    setTextColor(ContextCompat.getColor(context, R.color.purple_200))
}

2. Kotlin 自定义操作符

学习 Kotlin 一段时间后,你会发现 Kotlin 给了开发者很大的自我发展空间。比如:支持对类新增扩展函数,支持运算符重载等。所以,我们自己也可以自定义一些操作符,来方便开发。看过 Kotlin 自带的操作符实现的同学们会发现,这些函数都是 inline 关键字修饰的。我们先看下 inline 关键字。

inline 关键字,可以看做是一个是否 内联 的标记。被修饰的函数会在编译时,直接把函数体一起“拷贝”过去,就是将内联函数的代码直接放在内联函数的位置上,这与一般函数不同,在调用一般函数的时候,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回原来跳进来的地方继续执行后面的代码;而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行。这样做可以加快代码的运行速度,但是会增加编译时间以及编译后的代码量。

inline 关键字适合修饰不太复杂的但会频繁调用的函数。所以 Kotlin 自带的操作符都是 inline 函数,我们如果要自定义一个操作符,也是需要修饰为 inline 函数。如下就是自定义了一个 convert 操作符,功能类似集合中的 map 函数。

代码语言:javascript复制
// code 6
inline fun <T, E> Iterable<T>.convert(action: (T) -> E): Iterable<E> {
    val list: MutableList<E> = mutableListOf()
    for (item in this) list.add(action(item))
    return list
}

3. Kotlin 中反引号 ` 的用法

在前面的《Kotlin 学习笔记(一)》 中介绍了下 Kotlin 反引号处理 Kotlin 关键字在 Java 代码里冲突的问题。反引号还有一个作用,就是在 Kotlin 代码中将一个不合法的字符转变为合法字符。举个栗子:

代码语言:javascript复制
// code 7
object SmallTips {
    // 反引号可将非法字符转换为合法字符
    fun `123`(){
        println("函数名居然为`123`!")
    }
    fun ` `(){
        println("函数名居然为` `!")
    }
    fun `  `(){
        println("函数名居然为`  `!")
    }
}

// 调用也需将反引号加上
SmallTips.`123`()
SmallTips.` `()
SmallTips.`  `()

不可思议吧!函数名本来不能为纯数字或空格符,但是加上反引号就可以了!神奇!那么这有啥用?还记得 Kotlin 的 internal 访问修饰符吗?它限定了被它修饰的函数只能在当前模块使用,而不能在其他模块使用。但是 Java 中是没有这个修饰符的,而 Kotlin 和 Java 又必须完全兼容,所以 Java 也不得不支持这一特性。

那么问题来了,通过反编译查看 Kotlin 中 internal 修饰的函数,在生成的 Java 代码里被编译成了 public 修饰的函数(笑Cry.gif)。为了让 Java 不能访问 Kotlin 中的函数,可以在 Kotlin 中将这些函数的命名改为不合法的形式,然后用反引号包起来,这么做之后,Java 代码是不能调用这些方法的,而 Kotlin 可以调用,从而可以实现在 Java 中屏蔽某些 Kotlin 函数的效果。最后,这种反引号的用法不推荐使用!了解即可!

4. Kotlin 对象比较

在 Java 中,要比较两个对象是否相等,通常用的是 == 或 equals 方法。Java 中的 == 运算符比较的是两个对象本身的值,即两个对象在内存中的首地址。如果是两个字符串,就是比较的这两个字符串存储的地址是否是同一个。

Java 中,对象的首地址是它在内存中存放的起始地址,它后面的地址是用来存放它所包含的各个属性的地址,所以内存中会用多个内存块来存放对象的各个属性值,而通过这个首地址就可以找到该对象,进而可以找到该对象的各个属性。

Java 中的 equals 方法比较的是两个对象中各个属性值的是否相同。如果是两个字符串,就是比较的两字符串所包含的内容是否相同。

在 Kotlin 语言中,判断两个对象是否相等用的是 == 和 ===。没错,两个等号和三个等号。Kotlin 的 == 相当于 Java 中的 equals 方法;而 === 相当于 Java 中的 == 运算符,记住即可。栗子也有,看下方:

代码语言:javascript复制
// code 8
val str1 = java.lang.String("我发")
val str2 = java.lang.String("我发")
println("str1 == str2 结果为 ${str1 == str2}") // 输出:str1 == str2 结果为 true
println("str1 === str2 结果为 ${str1 === str2}") // 输出:str1 === str2 结果为 false

因为在 Kotlin 的 String 构造方法中,不能直接传入一个字符串,所以这里用的是 Java 中的 String 类进行的初始化。也可以用 Kotlin 的 String 另外两种初始化方法:1)val str1 = StringBuilder("我发").toString();2)val str1 = String("我发".toByteArray())。

5. Kotlin 的常量变量

根据笔记一中的内容,我们知道,Kotlin 有两种变量,一种是用 val 关键字修饰的不可变的变量;另一种是用 var 关键字修饰的可变的变量。如何在类中对这两种变量进行初始化呢?val 因为是不可变,所以只能重写变量的 getter 方法,var 则可以重写 getter 和 setter 方法,当然类会自动帮我们生成 getter 和 setter 方法。

代码语言:javascript复制
// code 9
class Person {    // 此类无实际意义,为了举个栗子而已
    var age: Int = 0 // 可变变量我们可以重写 getter 和 setter
        get() {
            return field.plus(10)
        }
        set(value) {
            field = value - 1
        }

    val name: String = "" // 不可变变量我们只能重写 getter 方法
        get() {
            return field   "haha"
        }

    val height: Float // height 是用 val 修饰的,但 height 并不是一个常量
        get() {
            return (age * 2F   10)
        }
}

在重写 getter 和 setter 方法时,可以通过 field 拿到该属性的值。val 和 var 最本质的区别就是,val 没有 setter 方法。val 并不是常量,而是说,我们不能再次对 val 变量进行赋值操作!为啥 val 修饰的并不是常量?可以看一下 code 9 中的 height 变量,当 age 变化时,height 也会变化,它并不是一个常量。

如果要声明一个常量,则要用到 const 关键字。它有两个注意点: 1)const 只能修饰 object 的属性,或 top-level 变量。 2)const 变量的值必须在编译期间就确定下来,所以类型只能是 String 或基本数据类型。

啥意思呢?我理解的就是,Kotlin 中用 const 修饰的常量类似于 Java 中的一个不可变的静态变量。它声明的地方只有三种1. object 类的内部,object 修饰的都是静态类;2. top-level 位置,也就是在一个类的外部进行声明;3. companion object 内部,也就是用于声明静态变量的位置。

代码语言:javascript复制
// code 10
object ValAndVarExample {
    const val t2 = "heiheihei"
}

const val t1 = "hahaha" // top-level,类外

class Person {
    companion object{
        const val t3 = "hehehe"
    }
}

6. Kotlin 的 inline、crossinline、noinline 关键字的特殊使用

在前面的第2节 Kotlin 的自定义操作符中,已经说明了 inline 关键字的基本用法,知道了内联函数可以通过直接将代码拷贝到调用的地方从而加快程序执行速度的特性。除了 inline 关键字外,还有 crossinline 和 noinline 两个关键字,来看看它们还有什么特殊的用法。

在讲之前,还是需要明白一些前提知识。inline 关键字既会影响函数对象本身,也会影响传入的 Lambda 闭包参数,两者都会被内联到调用点。

编译预处理器会对内联函数进行扩展,省去了参数压栈、生成汇编语言的 CALL 调用、返回参数、执行 return 等过程,从而提高运行速度。优点是,在函数被内联后编译器可以通过上下文相关的优化技术对结果代码执行更深入的优化;但会使得编译后的代码体积变大,只是省去了函数调用的开销。所以 inline 适合用于较简单的频繁调用的函数。

6.1. 被 inline 修饰的函数中的 Lambda 表达式,可以中断外部函数的调用。

啥意思?没关系,大家都是一脸懵。得结合例子说一下子:

代码语言:javascript复制
// code 11
fun main(args: Array<String>) {
    test1 {
        println("我要好好学 Kotlin")
        return
    }
    println("我要好好学习 Android")
}

inline fun test1(lambda: () -> Unit) {
    lambda.invoke()
}
// 输出:我要好好学 Kotlin

test1 函数被 inline 修饰,它有个 Lambda 闭包,在该闭包中有个 return 返回函数,这个函数可以中断外部的 main 函数,所以只会输出 “我要好好学 Kotlin”。

通常情况下,Kotlin 中函数内部 Lambda 闭包是不能中断外部函数的执行的,可以尝试下将 code 11 中 test1 修饰的 inline 去掉,此时编译器就会提示 return 只能写成 return@test1,即只能返回 test1 函数,并不能返回 main 函数。

6.2. crossinline 关键字不允许被 inline 修饰的函数中的 Lambda 表达式中断外部函数的执行。

意思就是,在 code 11 中,如果 Lambda 表达式的 return 只是想中断该闭包的执行,而不想中断外部 main 函数的执行,该咋办?有人会说,那我不用 inline 不就可以了?但这里又需要用 inline 呢?那就可以使用 crossinline 去修饰这个 Lambda 闭包,编译器就不会去对这个 Lambda 表达式做内联操作。

代码语言:javascript复制
// code 12
fun main(args: Array<String>) {
    test1 {
        println("我要好好学 Kotlin")
        return@test1
        println("我不想学习了~")
    }
    println("我要好好学习 Android")
} 

inline fun test1(crossinline lambda: () -> Unit) {
    lambda.invoke()
}
// 输出:
//我要好好学 Kotlin
//我要好好学习 Android
6.3. noinline 关键字不允许被 inline 修饰的函数中的 Lambda 表达式被内联处理。

首先,noinline 关键字是作用于 Lambda 闭包的;其次,它是用于在修饰了 inline 关键字的函数中,剔除 inline 关键字对 Lambda 闭包的影响,让它就作为一个普通的 Lambda 闭包。说明不够,代码来凑!

代码语言:javascript复制
// code 13
inline fun test2(lambda0: () -> Unit, noinline lambda1: () -> Unit): () -> Unit {
    lambda0.invoke()
    lambda1.invoke()
    return lambda1
}

test2 函数被 inline 修饰,有两个 Lambda 闭包作为参数,而且它的返回值也是一个 Lambda 闭包。如果 lambda1 没有 noinline 关键字修饰,那么它就会跟 lambda0 一样,将函数体直接拷贝到调用的地方,这种情况下,lambda1 就不能作为闭包返回了,所以去掉 noinline 之后,code 13 代码会报错。所以,这里如果要将 test2 用 inline 修饰,同时,又想返回一个闭包的话,就可以用 noinline 关键字去除 inline 对闭包的影响。

上面说的都是关于 inline 关键字的进阶用法,通常情况下不会用到,作为知识储备即可。

参考文献

  1. 张涛;极客时间 Kotlin 系列课程
  2. 深山里的小白羊;《内联函数》 https://blog.csdn.ne/qq_33757398。
  3. 韦邦杠;《java中equals以及==的用法(简单介绍)》 https://www.cnblogs.com/weibanggang/p/9457757.html。
  4. One_Month;《Kotlin中的noinline》 https://blog.csdn.net/One_Month/article/details/108980646。
  5. DONGYUXIA;《Kotlin基础之内联函数》 http://blog.chinaunix.net/uid-31478279-id-5782198.html。

ps. 赠人玫瑰,手留余香。欢迎转发分享加关注,你的认可是我继续创作的精神源泉。

0 人点赞