Kotlin语法
1. 作用域函数
Kotlin 提供了一系列作用域函数,它们可以让你在对象的特定作用域内执行代码,从而避免重复引用对象(如 this 或 it),提高代码简洁性与可读性。
1.1 apply - 在对象自身作用域内修改对象
- 返回值:调用者本身(this)
- 使用场景:
- 用于初始化对象,避免多次调用 set 方法
- 支持链式调用
示例:
1 | class Person { |
分析:
在 apply 的代码块中,this 代表当前对象,通过直接修改属性,最后返回修改后的对象本身。
1.2 let - 适用于非空值的操作
- 返回值:Lambda 表达式的最后一行结果
- 使用场景:
- 针对可空对象进行操作,避免 NullPointerException
- 控制变量的作用域
示例:
1 | val name: String? = "Kotlin" |
另一示例(变量作用域控制):
1 | val number = 5 |
1.3 run - 在对象作用域内执行代码并返回计算结果
- 返回值:Lambda 表达式的最后一行结果
- 使用场景:
- 需要在对象上执行多个操作并返回一个结果
示例:
1 | val personInfo = Person().run { |
分析:
run 在对象作用域内执行操作,返回最后一行表达式的结果,而不是对象本身。
1.4 also - 适用于对象的额外操作
- 返回值:调用者本身(this)
- 使用场景:
- 记录日志、调试等额外操作,不改变对象本身
示例:
1 | val numbers = mutableListOf(1, 2, 3).also { |
分析:
also 用于执行副作用(如日志打印),返回原对象,而后续的 apply 则对对象进行修改。
1.5 with - 用于非扩展对象的作用域调用
- 返回值:Lambda 表达式的最后一行结果
- 使用场景:
- 对非扩展对象执行一系列操作,并返回计算结果
示例:
1 | val person = Person() |
1.6 作用域函数对比总结
| 函数 | 使用场景 | 作用域内对象引用 | 返回值 |
|---|---|---|---|
| apply | 修改对象本身 | this |
对象本身 |
| let | 针对可空对象或局部变量 | it |
Lambda 最后一行结果 |
| run | 执行操作并返回计算结果 | this |
Lambda 最后一行结果 |
| also | 执行额外操作(日志、调试等) | it |
对象本身 |
| with | 对普通对象执行操作并返回结果 | this |
Lambda 最后一行结果 |
最佳实践提示:
- 修改对象并返回对象本身时,选择
apply - 针对可空对象操作时,选择
let - 需要返回计算结果时,选择
run或with - 仅执行副作用操作时,选择
also
2. 时间输出
下面的代码展示了如何在 Kotlin 中获取当前时间、格式化时间、解析字符串为日期,以及通过 Calendar 获取时间组件。
1 | package com.example.diary_final |
说明:
- 使用
SimpleDateFormat格式化和解析日期 Calendar用于提取当前时间的各个字段
3. 回调机制
3.1 什么是回调?
回调类似于“任务完成后通知我”。例如,让朋友去买咖啡,买完后打电话告诉你。在编程中,回调指的是任务完成后自动调用的函数。
3.2 示例:不使用回调(同步等待)
1 | fun buyGroceries() { |
问题:主线程会被阻塞,无法同时处理其他任务。
3.3 示例:使用回调(异步通知)
1 | fun buyGroceries(callback: () -> Unit) { |
分析:
使用回调后,任务在后台执行,主线程可继续执行其他操作,待任务完成后自动调用回调函数。
3.4 回调在 Android 开发中的应用
按钮点击事件:
1
2
3button.setOnClickListener {
println("按钮被点击了!") // 按钮点击后的回调
}网络请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14fun fetchData(callback: (String) -> Unit) {
Thread {
Thread.sleep(2000) // 模拟网络请求耗时
callback("数据加载成功了哈哈哈!")
}.start()
}
fun main() {
println("开始请求数据...")
fetchData { result ->
println(result) // 数据返回后的回调
}
println("请求已经发送,我先做别的事情")
}
4. Lambda 表达式
4.1 什么是 Lambda 表达式?
Lambda 表达式是匿名函数的一种写法,用于简化代码。其基本语法如下:
1 | { 参数 -> 表达式 } |
4.2 示例:基本用法
1 | // 定义一个 Lambda 表达式,接受两个 Int 参数并返回它们的和 |
4.3 Lambda 在高阶函数中的应用
1 | // 定义一个高阶函数,接受一个操作函数作为参数 |
4.4 单参数 Lambda 示例
当 Lambda 只有一个参数时,可以使用默认变量 it:
1 | val numbers = listOf(1, 2, 3, 4) |
5. 泛型与委托
5.1 泛型
泛型允许你编写适用于任意数据类型的通用代码,从而提高代码复用性和类型安全性。
5.1.1 泛型类示例
1 | // 定义一个泛型类 Box,可以存放任意类型的数据 |
5.1.2 泛型函数示例
1 | // 定义一个泛型函数,返回一个只包含一个元素的列表 |
5.1.3 类型参数的限定
1 | // 泛型 T 限制为 Number 或其子类 |
5.1.4 协变与逆变
(感觉不如书上P418讲得好)
1. 什么是协变和逆变?
它们解决的是泛型的子类型关系,也就是泛型类 A<T> 和 A<U> 之间是否可以互相赋值。
先看普通的子类型:
1 | kotlin复制编辑open class Parent |
这个没问题,Child 是 Parent 的子类。但如果是泛型呢?
1 | val childList: List<Child> = listOf(Child()) |
Kotlin 不允许 List<Child> 赋值给 List<Parent>,因为泛型默认是**不变(Invariant)**的,即 List<Child> 和 List<Parent> 没有任何继承关系。
协变(Covariance)—— out T
让 List<Child> 赋值给 List<Parent>
如果我们想让 List<Child> 赋值给 List<Parent>,就要用协变(out 关键字):
1 | interface Producer<out T> { // T 只能“生产”,不能“消费” |
为什么 out 只允许读取(生产)?
1 | interface Producer<out T> { |
out T只能作为返回值(生产者),不能作为参数(消费者)。- 因为
Producer<Child>赋值给Producer<Parent>后,如果允许consume(item: T),那就可能往Producer<Child>里面放Parent,导致类型错误。
示例:生产者
1 | class Book |
口诀:
Producer Out(生产者用
out,只能读,不能写)
逆变(Contravariance)—— in T
让 Consumer<Parent> 赋值给 Consumer<Child>
如果我们想让 Consumer<Parent> 赋值给 Consumer<Child>,就要用逆变(in 关键字):
1 | interface Consumer<in T> { // T 只能“消费”,不能“生产” |
为什么 in 只允许写入(消费)?
1 | interface Consumer<in T> { |
in T只能作为参数(消费者),不能作为返回值(生产者)。- 因为
Consumer<Parent>赋值给Consumer<Child>后,如果允许produce(),就可能返回Parent,但Consumer<Child>只能处理Child,导致类型错误。
示例:消费者
1 | class Animal |
口诀:
Consumer In(消费者用
in,只能写,不能读)
记住这两个原则
| 泛型类型 | 关键字 | 读取 | 写入 | 适用场景 |
|---|---|---|---|---|
| 协变 | out T |
✅ 允许 | ❌ 禁止 | 生产者(只读) |
| 逆变 | in T |
❌ 只能用 Any? 读取 |
✅ 允许 | 消费者(只写) |
最简单的记忆口诀
- “生产者用 out”(Producer Out) → 只能读,不能写。
- “消费者用 in”(Consumer In) → 只能写,不能安全地读。
生活中的例子
🔵 协变(out):只读,不写
假设你去看书 📖:
1 | kotlin |
- 你可以拿起一本书来看(
val book: Book = books[0]✅)。 - 但你不能往书架上随便放书(
books.add(Book())❌),因为这可能是科学书架,只能放《科学书》。
🔴 逆变(in):只写,不读
假设你有一个捐书箱 📦:
1 | kotlin |
- 你可以往里面放一本科学书(
donateBox.add(ScienceBook())✅)。 - 但你取出来的书不一定是科学书(
val sb: ScienceBook = donateBox[0]❌),可能是一本普通书,甚至是一本字典 📚。
6. 协变逆变总结
out(协变) → 只读
✅List<ScienceBook>可以赋值给List<out Book>,但不能add()。in(逆变) → 只写
✅MutableList<Book>可以赋值给MutableList<in ScienceBook>,但不能安全get()。in+out不能混用(同时in和out会报错)。
5.1.5 泛型实化(reified)
由于泛型在运行时会被擦除,使用 reified 可以保留类型信息:
1 | // 错误示例:无法在运行时获取 T 的类型信息 |
5.2 委托
委托是一种设计模式,可以将部分工作交由其他对象来处理,使代码更简洁和灵活。
5.2.1 属性委托
lazy 委托:延迟计算,首次访问时计算结果并缓存。
1 | // 定义一个 lazy 委托属性,只有首次访问时计算值 |
observable 委托:监听属性变化,每次属性改变时触发回调。
1 | import kotlin.properties.Delegates |
5.2.2 类委托
通过类委托,可以将接口的实现任务交由其他类处理,避免重复实现相同的方法。
1 | // 定义一个接口 |
总结
- 作用域函数:通过
apply、let、run、also和with简化对象操作,选择合适的函数可提高代码简洁性和可读性。 - 时间输出:使用
SimpleDateFormat和Calendar实现日期格式化、解析和时间组件获取。 - 回调机制:回调可以在任务完成时通知你,避免阻塞主线程,常用于 UI 事件、网络请求等场景。
- Lambda 表达式:用于编写匿名函数,简化代码,尤其在高阶函数中应用广泛。
- 泛型与委托:泛型提高代码通用性和类型安全;委托(属性委托和类委托)使代码更加简洁灵活,避免重复实现。
inline 函数是什么
在 Kotlin 中,inline 函数 是一种优化手段,其核心思想是在编译期将函数体直接替换到调用处,从而避免函数调用的开销,特别适用于高阶函数(即接受 lambda 参数的函数)。
1. 基本概念
- 目的:减少函数调用带来的性能开销,尤其在使用 lambda 表达式时,避免创建额外的对象。
- 工作原理:编译器在编译时将 inline 函数的代码“内联”(inline)到调用该函数的位置,而不是在运行时调用。
2. 使用场景
- 高阶函数:当函数接受 lambda 参数时,使用 inline 可以减少 lambda 对象的创建,提高性能。
- 小型函数:适用于那些频繁调用的小函数,可以提高效率。
- 泛型实化:与
reified关键字结合使用,可以在运行时获取泛型参数的具体类型。
3. 示例
3.1 基本示例
1 | // 定义一个 inline 函数 |
说明:在编译时,performOperation 的代码会直接内联到调用处,从而减少了函数调用的额外开销。
3.2 与 reified 结合
1 | // 使用 inline 和 reified 实现一个泛型函数,获取类型名称 |
说明:
- 使用
reified后,泛型参数在运行时不会被擦除,可以直接获取类型信息。 reified关键字必须和inline函数一起使用。
4. 注意事项
- 代码膨胀:由于 inline 函数会将函数体复制到每个调用处,如果函数体过大或者调用次数很多,可能会导致生成的字节码变大。
- 适用限制:某些情况下(例如递归调用)不适合使用 inline 函数。
inline函数总结
inline 函数主要用于优化高阶函数,通过在编译期内联函数体来减少运行时的函数调用开销。同时,它可以与 reified 结合使用,以在运行时保留泛型类型信息。虽然 inline 函数能够提高性能,但在使用时也需注意避免代码膨胀问题。