2、Kotlin安全特性:空安全、不可变性、密封类、协程安全、类型安全构建器
各位同学,咱们直接进入正题。Kotlin 这门语言,我用了快六年了。说实话,它最打动我的地方,不是语法糖多甜,而是它把「安全」两个字刻进了骨子里。今天这章,我就把几个最核心的安全特性掰开揉碎了讲给你听。
2.1 空安全:告别 NullPointerException
做 Android 的,谁没被 NullPointerException 坑过?我以前维护过一个老项目,线上崩溃有一半都是空指针。后来用 Kotlin 重写,这类问题直接少了八成。
Kotlin 的空安全,说白了就是「类型系统自带防呆设计」。你声明一个变量,默认就不允许为空:
var name: String = "Kotlin" // 不能赋 null
// name = null // 编译报错
如果你确实需要空值,得显式加个问号:
var nullableName: String? = null // 允许为空
嗯,这里要注意。一旦变量可空,你就不能直接调它的方法了。编译器会拦住你:
nullableName.length // 编译报错,不安全
你得用安全调用运算符 ?.:
val length = nullableName?.length // 如果为 null,返回 null
或者用 Elvis 运算符 ?: 给个默认值:
val length = nullableName?.length ?: 0
?.let{} 做局部处理,别让空值到处传播。
你想想看,这种设计好在哪?它把运行时才暴露的问题,提前到了编译阶段。我在项目中遇到过好几次,重构代码时漏判了空值,编译器直接标红——这比线上崩溃强一万倍。
2.2 不可变性:用 val 守住数据边界
Kotlin 提供了 val 和 var 两种声明方式。我个人建议:能用 val 就别用 var。
val 是只读引用,一旦赋值就不能再改。这能防止很多「手滑」操作:
val config = loadConfig() // 只读
// config = otherConfig // 编译报错
但要注意,val 只保证引用不变,不保证对象内部状态不变。比如:
val list = mutableListOf(1, 2, 3)
list.add(4) // 可以,因为 list 本身是可变的
真要彻底不可变,得用 listOf()、mapOf() 这些不可变集合:
val immutableList = listOf(1, 2, 3)
// immutableList.add(4) // 编译报错
val list = mutableListOf() 传到了多个线程里。val 没变,但 list 内部被并发修改了。所以,不可变性要「内外兼修」。
为什么强调不可变性?多线程环境下,可变状态是万恶之源。你想想看,一个变量被两个线程同时读写,结果能好吗?不可变对象天然线程安全,省心多了。
2.3 密封类:用类型系统约束可能性
密封类(sealed class)是我在项目中用得越来越频繁的特性。它本质上是一种受限的类层次结构——所有子类都必须在同一个文件里定义。
举个例子,网络请求的状态:
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val message: String, val code: Int) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}
用 when 表达式处理时,编译器会强制你覆盖所有分支:
fun handleResult(result: NetworkResult<String>) {
when (result) {
is NetworkResult.Success -> println("成功: ${result.data}")
is NetworkResult.Error -> println("失败: ${result.message}")
is NetworkResult.Loading -> println("加载中")
}
// 不需要 else 分支,因为所有情况都覆盖了
}
这有什么好处?你加一个新子类,编译器会提醒你所有 when 分支都要更新。我在项目中遇到过,有人新增了一个 Timeout 状态,结果忘了更新处理逻辑——如果有密封类,编译阶段就能发现。
2.4 协程安全:结构化并发与取消
协程是 Kotlin 的并发利器,但用不好也会出问题。我见过不少同学,协程启动了一堆,结果 Activity 销毁了还在跑——这不就是内存泄漏吗?
Kotlin 的结构化并发,就是为了解决这个问题。每个协程都有明确的「作用域」:
lifecycleScope.launch {
// 这个协程的生命周期跟随 Activity/Fragment
val data = withContext(Dispatchers.IO) {
fetchDataFromNetwork()
}
updateUI(data)
}
当 Activity 销毁时,lifecycleScope 会自动取消所有子协程。你不用手动管理,省心。
但要注意,协程的取消是「协作式」的。如果你的协程里有耗时操作,得检查取消状态:
lifecycleScope.launch {
for (i in 0 until 1000) {
ensureActive() // 检查是否被取消
doHeavyWork(i)
}
}
GlobalScope.launch 启动协程,结果用户切了几个页面后,后台还在跑网络请求。后来全部改成 lifecycleScope 或 viewModelScope,问题就解决了。记住:别用 GlobalScope,除非你真的知道自己在做什么。
另外,协程的异常处理也要注意。用 try-catch 包裹协程体,或者用 CoroutineExceptionHandler:
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("TAG", "协程异常: ${exception.message}")
}
lifecycleScope.launch(handler) {
// 可能抛出异常的代码
}
2.5 类型安全构建器:用 DSL 写出声明式代码
类型安全构建器(Type-Safe Builders)是 Kotlin 的一个高级特性。它让你能用声明式的方式构建复杂对象,比如布局、配置、请求参数等。
最经典的例子是 Anko 布局(虽然现在用得少了),但原理是一样的:
fun buildHtml() = html {
head {
title { +"我的页面" }
}
body {
h1 { +"欢迎" }
p {
+"这是一个段落"
}
}
}
这种写法的好处是:类型安全。你只能在 head 里写 title,不能在 body 里写。编译器会帮你检查。
我自己在项目中常用构建器来构建网络请求参数:
val request = httpRequest {
url("https://api.example.com/data")
method = HttpMethod.GET
headers {
"Authorization" to "Bearer token"
"Content-Type" to "application/json"
}
queryParams {
"page" to 1
"size" to 20
}
}
实现一个简单的构建器,核心是「带接收者的 lambda」:
class HttpRequestBuilder {
var url: String = ""
var method: HttpMethod = HttpMethod.GET
private val headers = mutableMapOf<String, String>()
private val params = mutableMapOf<String, Any>()
fun headers(block: MutableMap<String, String>.() -> Unit) {
headers.apply(block)
}
fun queryParams(block: MutableMap<String, Any>.() -> Unit) {
params.apply(block)
}
fun build(): HttpRequest = HttpRequest(url, method, headers, params)
}
fun httpRequest(block: HttpRequestBuilder.() -> Unit): HttpRequest {
return HttpRequestBuilder().apply(block).build()
}
好了,这五个特性讲完了。你发现没有?它们都有一个共同点——把错误扼杀在编译阶段。空安全防空指针,不可变性防并发问题,密封类防遗漏分支,协程安全防泄漏,类型安全构建器防拼写错误。说白了,Kotlin 的设计哲学就是:能编译时解决的问题,绝不拖到运行时。
下一章,我会讲 Kotlin 的加密与数据保护。到时候见。