1. Kotlin基础回顾:变量与常量、空安全机制、函数与Lambda表达式、数据类与密封类
各位同学,欢迎来到《Kotlin Android MVVM与Clean架构实战》的第一章。
说实话,每次带新人或者做技术分享,我都会从Kotlin基础开始讲。为什么?因为很多坑,其实都出在基础不牢上。我自己刚转Kotlin那会儿,也踩过不少雷。今天这一章,咱们就把地基打扎实。
本章核心知识图谱
1.1 变量与常量:val 和 var 的选择
在Kotlin里,声明变量有两种方式:val 和 var。很多人觉得这有啥好讲的?不就是可变和不可变嘛。但我在项目中见过太多滥用 var 的情况了。
val 是只读引用,一旦赋值就不能再改。注意,我说的是「引用」不能改,如果它指向一个对象,对象内部的状态是可以变的。var 才是真正的可变变量。
我的习惯:能用 val 就用 val。你想想看,一个变量如果永远不会被重新赋值,代码的可读性和安全性都会提升。我重构老项目时,第一步就是把能改成 val 的 var 全改掉。
// 正确的用法
val name: String = "Kotlin" // 只读,不能重新赋值
var count: Int = 0 // 可变
// 注意:val 指向的对象内部可以变化
val list = mutableListOf<Int>(1, 2, 3)
list.add(4) // 这是允许的
// list = mutableListOf(5, 6, 7) // 编译错误!
再说说 lateinit 和 lazy。这两个都是延迟初始化的手段,但适用场景完全不同。
| 特性 | lateinit | lazy |
|---|---|---|
| 适用类型 | var 可变属性 | val 只读属性 |
| 初始化时机 | 手动赋值 | 首次访问时自动执行 |
| 线程安全 | 不保证 | 默认线程安全 |
| 常见场景 | 依赖注入、Activity/Fragment 中的 View | 耗时计算、单例 |
我曾经踩过的坑:在 Activity 里用 lateinit 声明了一个 View 变量,结果在 onCreate 之前就访问了它,直接抛 UninitializedPropertyAccessException。记住,lateinit 只是延迟了声明,不是延迟了初始化,你得确保在使用前已经赋值。
1.2 空安全机制:Kotlin 最让我安心的设计
说实话,Java 里最让我头疼的就是 NullPointerException。Kotlin 的空安全机制,说白了就是把这个运行时问题提到了编译期。我个人觉得,这是 Kotlin 最值得吹爆的特性。
Kotlin 的类型系统默认不允许空值。如果你想允许一个变量为 null,必须显式地在类型后面加 ?。
// 不可空类型
var name: String = "Hello"
// name = null // 编译错误!
// 可空类型
var nullableName: String? = "Hello"
nullableName = null // 没问题
// 安全调用操作符 ?.
val length = nullableName?.length // 如果 nullableName 为 null,结果为 null
// Elvis 操作符 ?:
val lengthOrZero = nullableName?.length ?: 0 // 为 null 时返回 0
// 非空断言 !! (慎用!)
val forcedLength = nullableName!!.length // 为 null 时抛 NPE
核心原则:能用 ?. 就别用 !!。我见过太多人为了省事直接上 !!,结果线上崩得稀里哗啦。你想想看,Kotlin 费这么大劲帮你避免 NPE,你一个 !! 又全给找回来了。
还有一个常用的操作符是 let。配合安全调用,可以优雅地处理可空对象:
nullableName?.let {
// 这里面的 it 一定不为 null
println("名字长度是 ${it.length}")
}
1.3 函数与 Lambda 表达式
Kotlin 的函数比 Java 灵活得多。我个人最喜欢的是默认参数和命名参数,写工具类的时候特别爽。
// 带默认参数的函数
fun greet(name: String, greeting: String = "你好") {
println("$greeting, $name")
}
// 调用时可以省略有默认值的参数
greet("小明") // 输出:你好, 小明
greet("小明", "Hello") // 输出:Hello, 小明
// 命名参数,提高可读性
greet(greeting = "嗨", name = "小红")
Lambda 表达式是 Kotlin 函数式编程的核心。说白了,Lambda 就是一段可以作为参数传递的代码块。
// Lambda 的基本语法
val sum: (Int, Int) -> Int = { x, y -> x + y }
println(sum(3, 5)) // 输出 8
// 如果 Lambda 是函数的最后一个参数,可以提到括号外面
val numbers = listOf(1, 2, 3, 4, 5)
numbers.filter { it > 2 }
.map { it * 2 }
.forEach { println(it) } // 输出 6, 8, 10
高阶函数实战:在 MVVM 架构中,我们经常用 Lambda 来做回调。比如 ViewModel 暴露一个 LiveData,Fragment 通过观察者模式来响应数据变化。这背后其实就是高阶函数的思想。
1.4 数据类与密封类
数据类 data class 是我在项目中用得最多的特性之一。你想想看,Java 里写一个 POJO 要生成多少样板代码?getter、setter、toString、equals、hashCode……Kotlin 一行搞定。
data class User(
val id: Long,
val name: String,
val email: String? = null
)
// 自动生成了以下方法:
// - toString():User(id=1, name=Alice, email=alice@example.com)
// - equals() / hashCode():基于主构造函数中的所有属性
// - copy():方便复制并修改部分属性
// - componentN():解构声明
val user = User(1, "Alice")
val copyUser = user.copy(name = "Bob")
println(copyUser) // User(id=1, name=Bob, email=null)
注意:data class 的主构造函数中至少有一个参数。而且,自动生成的方法只基于主构造函数中的属性。如果你在类体中声明属性,它们不会被包含在 equals、hashCode 和 toString 中。我曾经因为这个排查了半天 bug,大家引以为戒。
密封类 sealed class 是 Kotlin 中一个非常强大的概念。它用来表示受限的类层次结构,说白了就是「这个类的子类是有限的,就这么多」。配合 when 表达式使用,可以实现穷举检查。
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("成功:${result.data}")
is Result.Error -> println("失败:${result.message}")
Result.Loading -> println("加载中...")
// 不需要 else 分支,因为已经穷举了所有可能
}
}
在 MVVM 架构中,我经常用密封类来表示 UI 状态。比如一个页面的加载状态,无非就是加载中、成功、失败这三种。用密封类来建模,编译器会帮你检查是否处理了所有情况,非常安全。
总结一下:数据类帮我们省去了大量样板代码,密封类帮我们构建安全的有限状态机。这两个特性在 Clean 架构中会频繁出现,尤其是定义 Repository 的返回结果或者 UI 状态时。
好了,第一章的内容就到这里。这些基础概念虽然简单,但它们是后续所有高级特性的基石。我个人建议,如果你对某个点还不太熟悉,最好打开 IDE 敲一遍。代码这东西,光看是学不会的。