1. 作用域函数

Kotlin 提供了一系列作用域函数,它们可以让你在对象的特定作用域内执行代码,从而避免重复引用对象(如 thisit),提高代码简洁性与可读性。

1.1 apply - 在对象自身作用域内修改对象

  • 返回值:调用者本身(this)
  • 使用场景
    • 用于初始化对象,避免多次调用 set 方法
    • 支持链式调用

示例:

1
2
3
4
5
6
7
8
9
10
11
class Person {
var name: String = ""
var age: Int = 0
}

val person = Person().apply {
name = "Alice"
age = 20
}
println(person.name) // Alice
println(person.age) // 20

分析
apply 的代码块中,this 代表当前对象,通过直接修改属性,最后返回修改后的对象本身。


1.2 let - 适用于非空值的操作

  • 返回值:Lambda 表达式的最后一行结果
  • 使用场景
    • 针对可空对象进行操作,避免 NullPointerException
    • 控制变量的作用域

示例:

1
2
3
4
val name: String? = "Kotlin"
name?.let {
println(it.uppercase()) // KOTLIN
}

另一示例(变量作用域控制):

1
2
3
4
5
6
val number = 5
val square = number.let {
val result = it * it
result
}
println(square) // 25

1.3 run - 在对象作用域内执行代码并返回计算结果

  • 返回值:Lambda 表达式的最后一行结果
  • 使用场景
    • 需要在对象上执行多个操作并返回一个结果

示例:

1
2
3
4
5
6
val personInfo = Person().run {
name = "Bob"
age = 25
"名字是 $name, 年龄是 $age"
}
println(personInfo) // 名字是 Bob, 年龄是 25

分析
run 在对象作用域内执行操作,返回最后一行表达式的结果,而不是对象本身。


1.4 also - 适用于对象的额外操作

  • 返回值:调用者本身(this)
  • 使用场景
    • 记录日志、调试等额外操作,不改变对象本身

示例:

1
2
3
4
5
6
val numbers = mutableListOf(1, 2, 3).also {
println("原始列表: $it") // 输出:原始列表: [1, 2, 3]
}.apply {
add(4)
}
println(numbers) // 输出:[1, 2, 3, 4]

分析
also 用于执行副作用(如日志打印),返回原对象,而后续的 apply 则对对象进行修改。


1.5 with - 用于非扩展对象的作用域调用

  • 返回值:Lambda 表达式的最后一行结果
  • 使用场景
    • 对非扩展对象执行一系列操作,并返回计算结果

示例:

1
2
3
4
5
6
7
val person = Person()
val result = with(person) {
name = "Charlie"
age = 30
"名字: $name, 年龄: $age"
}
println(result) // 输出:名字: Charlie, 年龄: 30

1.6 作用域函数对比总结

函数 使用场景 作用域内对象引用 返回值
apply 修改对象本身 this 对象本身
let 针对可空对象或局部变量 it Lambda 最后一行结果
run 执行操作并返回计算结果 this Lambda 最后一行结果
also 执行额外操作(日志、调试等) it 对象本身
with 对普通对象执行操作并返回结果 this Lambda 最后一行结果

最佳实践提示:

  • 修改对象并返回对象本身时,选择 apply
  • 针对可空对象操作时,选择 let
  • 需要返回计算结果时,选择 runwith
  • 仅执行副作用操作时,选择 also

2. 时间输出

下面的代码展示了如何在 Kotlin 中获取当前时间、格式化时间、解析字符串为日期,以及通过 Calendar 获取时间组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.diary_final
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale

fun main() {
// 获取当前时间,并格式化为字符串
val date = Date() // 当前时间
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val formattedDate = sdf.format(date)
println(formattedDate) // 输出格式化日期,例如:2025-03-15 14:08:23

// 将字符串解析为 Date 类型
val dateString = "2025-03-09 14:45:30"
val sdfp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val datep = sdfp.parse(dateString)
println(datep) // 输出: Sun Mar 09 14:45:30 GMT 2025

// 使用 Calendar 获取当前时间的时、分、秒
val calendar = Calendar.getInstance()
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
println("当前时间: $hour:$minute:$second")

// 格式化输出时间(24小时制)
val formattedTime = String.format("%02d:%02d:%02d", hour, minute, second)
println("当前时间: $formattedTime")
println("当前时间: ${String.format("%02d:%02d:%02d", hour, minute, second)}")
}

说明:

  • 使用 SimpleDateFormat 格式化和解析日期
  • Calendar 用于提取当前时间的各个字段

3. 回调机制

3.1 什么是回调?

回调类似于“任务完成后通知我”。例如,让朋友去买咖啡,买完后打电话告诉你。在编程中,回调指的是任务完成后自动调用的函数。

3.2 示例:不使用回调(同步等待)

1
2
3
4
5
6
7
8
9
10
fun buyGroceries() {
Thread.sleep(2000) // 模拟买东西耗时2秒
println("买完了!") // 买完后通知
}

fun main() {
println("请帮我去买东西...")
buyGroceries()
println("我收到通知了") // 等待任务完成后执行
}

问题:主线程会被阻塞,无法同时处理其他任务。


3.3 示例:使用回调(异步通知)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun buyGroceries(callback: () -> Unit) {
Thread {
Thread.sleep(2000) // 模拟买东西耗时2秒
println("买完了!")
callback() // 任务完成后执行回调
}.start()
}

fun main() {
println("请帮我去买东西...")
buyGroceries {
println("我收到通知了!") // 任务完成后调用回调
}
println("我可以做别的事情,不用等")
}

分析
使用回调后,任务在后台执行,主线程可继续执行其他操作,待任务完成后自动调用回调函数。


3.4 回调在 Android 开发中的应用

  • 按钮点击事件:

    1
    2
    3
    button.setOnClickListener {
    println("按钮被点击了!") // 按钮点击后的回调
    }
  • 网络请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fun 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
2
3
// 定义一个 Lambda 表达式,接受两个 Int 参数并返回它们的和
val add: (Int, Int) -> Int = { a, b -> a + b }
println(add(2, 3)) // 输出:5

4.3 Lambda 在高阶函数中的应用

1
2
3
4
5
6
7
8
9
10
// 定义一个高阶函数,接受一个操作函数作为参数
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}

fun main() {
// 使用尾随 Lambda 语法
val result = calculate(10, 5) { x, y -> x + y }
println(result) // 输出:15
}

4.4 单参数 Lambda 示例

当 Lambda 只有一个参数时,可以使用默认变量 it

1
2
3
val numbers = listOf(1, 2, 3, 4)
val doubled = numbers.map { it * 2 }
println(doubled) // 输出:[2, 4, 6, 8]

5. 泛型与委托

5.1 泛型

泛型允许你编写适用于任意数据类型的通用代码,从而提高代码复用性和类型安全性。

5.1.1 泛型类示例

1
2
3
4
5
6
7
8
9
// 定义一个泛型类 Box,可以存放任意类型的数据
class Box<T>(var value: T)

fun main() {
val intBox = Box(123) // T 被推断为 Int
val strBox = Box("Hello") // T 被推断为 String
println(intBox.value) // 输出:123
println(strBox.value) // 输出:Hello
}

5.1.2 泛型函数示例

1
2
3
4
5
6
7
8
9
10
11
// 定义一个泛型函数,返回一个只包含一个元素的列表
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}

fun main() {
val numbers = singletonList(42) // T 被推断为 Int
val words = singletonList("Kotlin") // T 被推断为 String
println(numbers) // 输出:[42]
println(words) // 输出:[Kotlin]
}

5.1.3 类型参数的限定

1
2
3
4
5
6
7
8
9
// 泛型 T 限制为 Number 或其子类
fun <T : Number> add(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}

fun main() {
println(add(10, 20)) // 输出:30.0
// println(add("a", "b")) // 错误,String 不是 Number 的子类
}

5.1.4 协变与逆变

(感觉不如书上P418讲得好)

1. 什么是协变和逆变?

它们解决的是泛型的子类型关系,也就是泛型类 A<T>A<U> 之间是否可以互相赋值

先看普通的子类型:

1
2
3
4
kotlin复制编辑open class Parent
class Child : Parent()

val parent: Parent = Child() // ✅ 子类可以赋值给父类

这个没问题,ChildParent 的子类。但如果是泛型呢?

1
2
val childList: List<Child> = listOf(Child())
// val parentList: List<Parent> = childList // ❌ 报错!

Kotlin 不允许 List<Child> 赋值给 List<Parent>,因为泛型默认是**不变(Invariant)**的,即 List<Child>List<Parent> 没有任何继承关系。


协变(Covariance)—— out T

List<Child> 赋值给 List<Parent>

如果我们想让 List<Child> 赋值给 List<Parent>,就要用协变out 关键字):

1
2
3
4
5
6
7
8
9
10
 interface Producer<out T> { // T 只能“生产”,不能“消费”
fun produce(): T
}

val childProducer: Producer<Child> = object : Producer<Child> {
override fun produce(): Child = Child()
}

// ✅ 可以把 Producer<Child> 赋值给 Producer<Parent>
val parentProducer: Producer<Parent> = childProducer

为什么 out 只允许读取(生产)?

1
2
3
4
interface Producer<out T> {
fun produce(): T // ✅ 只能提供 T
// fun consume(item: T) // ❌ 错误,不能接收 T
}
  • out T 只能作为返回值(生产者),不能作为参数(消费者)。
  • 因为 Producer<Child> 赋值给 Producer<Parent> 后,如果允许 consume(item: T),那就可能往 Producer<Child> 里面放 Parent,导致类型错误。

示例:生产者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Book
class ScienceBook : Book()

fun getScienceBooks(): List<ScienceBook> {
return listOf(ScienceBook())
}

// ✅ 使用 `out T` 让子类泛型可以赋值给父类泛型
fun readBooks(books: List<out Book>) {
for (book in books) {
println(book)
}
}

val scienceBooks = getScienceBooks()
readBooks(scienceBooks) // ✅ OK,因为 List<ScienceBook> 可以赋值给 List<out Book>

口诀:

Producer Out(生产者用 out,只能读,不能写)


逆变(Contravariance)—— in T

Consumer<Parent> 赋值给 Consumer<Child>

如果我们想让 Consumer<Parent> 赋值给 Consumer<Child>,就要用逆变in 关键字):

1
2
3
4
5
6
7
8
9
10
11
12
interface Consumer<in T> { // T 只能“消费”,不能“生产”
fun consume(item: T)
}

val parentConsumer: Consumer<Parent> = object : Consumer<Parent> {
override fun consume(item: Parent) {
println("Consumed a Parent")
}
}

// ✅ 可以把 Consumer<Parent> 赋值给 Consumer<Child>
val childConsumer: Consumer<Child> = parentConsumer

为什么 in 只允许写入(消费)?

1
2
3
4
interface Consumer<in T> {
fun consume(item: T) // ✅ 只能消费 T
// fun produce(): T // ❌ 错误,不能返回 T
}
  • in T 只能作为参数(消费者),不能作为返回值(生产者)。
  • 因为 Consumer<Parent> 赋值给 Consumer<Child> 后,如果允许 produce(),就可能返回 Parent,但 Consumer<Child> 只能处理 Child,导致类型错误。

示例:消费者

1
2
3
4
5
6
7
8
9
10
class Animal
class Dog : Animal()

fun trainDogs(trainers: MutableList<in Dog>) {
trainers.add(Dog()) // ✅ 可以添加 Dog
// val dog: Dog = trainers[0] // ❌ 不能安全地读取
}

val animals = mutableListOf<Animal>()
trainDogs(animals) // ✅ OK,因为 `MutableList<in Dog>` 允许 `MutableList<Animal>` 作为参数

口诀:

Consumer In(消费者用 in,只能写,不能读)


记住这两个原则
泛型类型 关键字 读取 写入 适用场景
协变 out T ✅ 允许 ❌ 禁止 生产者(只读)
逆变 in T ❌ 只能用 Any? 读取 ✅ 允许 消费者(只写)

最简单的记忆口诀

  • “生产者用 out”(Producer Out) → 只能读,不能写。
  • “消费者用 in”(Consumer In) → 只能写,不能安全地读。

生活中的例子

🔵 协变(out):只读,不写

假设你去看书 📖:

1
2
3
4
5
kotlin


复制编辑
val books: List<out Book> = listOf(ScienceBook())
  • 你可以拿起一本书来看val book: Book = books[0] ✅)。
  • 但你不能往书架上随便放书books.add(Book()) ❌),因为这可能是科学书架,只能放《科学书》。

🔴 逆变(in):只写,不读

假设你有一个捐书箱 📦:

1
2
3
4
5
kotlin


复制编辑
val donateBox: MutableList<in ScienceBook> = mutableListOf<Book>()
  • 你可以往里面放一本科学书donateBox.add(ScienceBook()) ✅)。
  • 但你取出来的书不一定是科学书val sb: ScienceBook = donateBox[0] ❌),可能是一本普通书,甚至是一本字典 📚。

6. 协变逆变总结

  1. out(协变) → 只读
    List<ScienceBook> 可以赋值给 List<out Book>,但不能 add()
  2. in(逆变) → 只写
    MutableList<Book> 可以赋值给 MutableList<in ScienceBook>,但不能安全 get()
  3. in + out 不能混用(同时 inout 会报错)。

5.1.5 泛型实化(reified)

由于泛型在运行时会被擦除,使用 reified 可以保留类型信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误示例:无法在运行时获取 T 的类型信息
fun <T> getType(): String {
return T::class.java.name // ❌
}

// 正确示例:使用 reified 保留类型信息
inline fun <reified T> getType(): String {
return T::class.java.name
}

fun main() {
println(getType<String>()) // 输出: java.lang.String
println(getType<Int>()) // 输出: int
}

5.2 委托

委托是一种设计模式,可以将部分工作交由其他对象来处理,使代码更简洁和灵活。

5.2.1 属性委托

lazy 委托:延迟计算,首次访问时计算结果并缓存。

1
2
3
4
5
6
7
8
9
10
11
// 定义一个 lazy 委托属性,只有首次访问时计算值
val lazyValue: String by lazy {
println("开始计算")
"Hello, Kotlin"
}

fun main() {
println("程序开始")
println(lazyValue) // 第一次调用时输出计算过程和结果
println(lazyValue) // 第二次调用时直接输出结果,无重复计算
}

observable 委托:监听属性变化,每次属性改变时触发回调。

1
2
3
4
5
6
7
8
9
import kotlin.properties.Delegates

var name: String by Delegates.observable("初始值11") { property, oldValue, newValue ->
println("属性 ${property.name} 从 $oldValue 变成了 $newValue")
}

fun main() {
name = "Kotlin22" // 输出:属性 name 从 初始值11 变成了 Kotlin22
}

5.2.2 类委托

通过类委托,可以将接口的实现任务交由其他类处理,避免重复实现相同的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个接口
interface Printer {
fun print()
}

// 定义接口实现类
class PrinterImpl(val message: String) : Printer {
override fun print() {
println(message)
}
}

// 类委托:DelegatingPrinter 将 Printer 接口的实现委托给传入的 printer 对象
class DelegatingPrinter(printer: Printer) : Printer by printer

fun main() {
val printerImpl = PrinterImpl("Hello from PrinterImpl")
val delegatingPrinter = DelegatingPrinter(printerImpl)
delegatingPrinter.print() // 实际调用 PrinterImpl 的 print() 方法
}

总结

  • 作用域函数:通过 applyletrunalsowith 简化对象操作,选择合适的函数可提高代码简洁性和可读性。
  • 时间输出:使用 SimpleDateFormatCalendar 实现日期格式化、解析和时间组件获取。
  • 回调机制:回调可以在任务完成时通知你,避免阻塞主线程,常用于 UI 事件、网络请求等场景。
  • Lambda 表达式:用于编写匿名函数,简化代码,尤其在高阶函数中应用广泛。
  • 泛型与委托:泛型提高代码通用性和类型安全;委托(属性委托和类委托)使代码更加简洁灵活,避免重复实现。

inline 函数是什么

在 Kotlin 中,inline 函数 是一种优化手段,其核心思想是在编译期将函数体直接替换到调用处,从而避免函数调用的开销,特别适用于高阶函数(即接受 lambda 参数的函数)。


1. 基本概念

  • 目的:减少函数调用带来的性能开销,尤其在使用 lambda 表达式时,避免创建额外的对象。
  • 工作原理:编译器在编译时将 inline 函数的代码“内联”(inline)到调用该函数的位置,而不是在运行时调用。

2. 使用场景

  • 高阶函数:当函数接受 lambda 参数时,使用 inline 可以减少 lambda 对象的创建,提高性能。
  • 小型函数:适用于那些频繁调用的小函数,可以提高效率。
  • 泛型实化:与 reified 关键字结合使用,可以在运行时获取泛型参数的具体类型。

3. 示例

3.1 基本示例

1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个 inline 函数
inline fun performOperation(operation: () -> Unit) {
println("操作开始")
operation()
println("操作结束")
}

fun main() {
performOperation {
println("执行具体操作")
}
}

说明:在编译时,performOperation 的代码会直接内联到调用处,从而减少了函数调用的额外开销。


3.2 与 reified 结合

1
2
3
4
5
6
7
8
// 使用 inline 和 reified 实现一个泛型函数,获取类型名称
inline fun <reified T> getTypeName(): String {
return T::class.java.name
}

fun main() {
println(getTypeName<String>()) // 输出: java.lang.String
}

说明

  • 使用 reified 后,泛型参数在运行时不会被擦除,可以直接获取类型信息。
  • reified 关键字必须和 inline 函数一起使用。

4. 注意事项

  • 代码膨胀:由于 inline 函数会将函数体复制到每个调用处,如果函数体过大或者调用次数很多,可能会导致生成的字节码变大。
  • 适用限制:某些情况下(例如递归调用)不适合使用 inline 函数。

inline函数总结

inline 函数主要用于优化高阶函数,通过在编译期内联函数体来减少运行时的函数调用开销。同时,它可以与 reified 结合使用,以在运行时保留泛型类型信息。虽然 inline 函数能够提高性能,但在使用时也需注意避免代码膨胀问题。