KMP学习day4--Kotlin空安全与泛型
一、引入:null 为什么被称为「十亿美元的错误」
null 引用是 1965 年 Tony Hoare 在设计 ALGOL W 时随手引入的,几十年后他自己道歉称其为「十亿美元的错误」。Java 程序员对 NullPointerException 再熟悉不过:线上崩溃排行榜常年第一。ArkTS 则从语言层面禁用了 null,用 undefined + 可选链来规避。
Kotlin 的空安全走的是第三条路:类型系统直接区分「可空」与「不可空」,让编译器在你写代码的那一刻就拦住潜在的 NPE。这不是运行时检查,而是编译期约束------错误在你按下运行之前就暴露了。
泛型则是另一块日常高频使用的基石:集合全靠它,封装通用逻辑也靠它。这篇文章把 Kotlin 的空安全三件套和泛型上界约束一次讲清,并全程对照 ArkTS 的写法。
二、空安全的核心:类型系统里的问号
Kotlin 中每个类型默认不可为 null:
kotlin
var name: String = "张三"
name = null // ❌ 编译错误:String 不能是 null
var nickname: String? = null // ✅ String? 才可以
就一个 ? 的差别,但编译器从此对两种类型的检查完全不同:String 保证永远不为 null,你可以放心调用它的任何方法;String? 则随时可能是 null,编译器禁止你直接调用它的方法:
kotlin
val len1: Int = name.length // ✅ name 不可能为 null
val len2: Int = nickname.length // ❌ 编译错误:只有安全调用或判空后才可访问
这就是 Kotlin 堵 NPE 的根本思路:把「可能为 null」从文档和注释里拉出来,直接写进类型签名 。函数参数是 String? 还是 String,调用方一眼就知道要不要判空,不用翻文档。
三、空安全三件套:?.、?:、!!
处理可空值有三个核心操作符,俗称三件套:
1. 安全调用 ?.------为 null 就整个表达式返回 null
kotlin
val nickname: String? = null
println(nickname?.length) // null,不崩溃
println(name?.length) // 2(如果 name = "张三")
a?.b 等价于「如果 a 为 null 返回 null,否则返回 a.b」。链式调用时特别好用:
kotlin
val city = user?.address?.city // 任何一环为 null,整体就是 null
2. Elvis 操作符 ?:------为 null 时给个兜底值
kotlin
val displayName: String = nickname ?: "匿名用户"
?: 右侧是左侧为 null 时的替代值。因为左侧 nickname?.length 可能是 null,而右侧兜了底,所以整个表达式可以安全地赋给非空类型。还可以配合 return / throw 提前退出:
kotlin
val uid = userId ?: return // 没有就直接返回
val config = loadConfig() ?: error("配置缺失") // error 抛异常
3. 非空断言 !!------「我保证不是 null,崩了算我的」
kotlin
val sure: String = nickname!! // 若真是 null,立刻抛 NPE
!! 把编译期检查硬转回运行时崩溃,等于亲手放弃了空安全的保护。风格共识:只在 100% 确定且能用其他方式证明的地方使用,日常代码出现 !! 通常说明设计有问题 ------要么类型标错了,要么应该用 ?. + ?: 兜底。
把三件套组合起来,一行就能安全地完成「取值 + 兜底」:
kotlin
fun displayNameLength(nickname: String?): Int = nickname?.length ?: 0
四、与 ArkTS 对照
ArkTS 用可选类型 string | undefined 加可选链表达同样的意思:
typescript
let nickname: string | undefined = undefined
let len: number = nickname?.length ?? 0 // ?. 与 ?? 组合
对照表:
| Kotlin | ArkTS | 说明 |
|---|---|---|
String |
string |
不可空 |
String? |
`string | null`(或 undefined) |
?. |
?. |
安全调用,语义几乎一致 |
?: |
?? |
空值兜底 |
!! |
!(非空断言) |
都是关掉检查的逃生门 |
设计差异在于:ArkTS 混用 null/undefined 两个「空」,Kotlin 只有一个 null,类型规则更简单;且 Kotlin 的编译器流分析(判空后智能转型)更强------if (nickname != null) 之后的分支里,nickname 会自动被当作 String 使用,不需要强转。
五、泛型:让类型成为参数
泛型解决的问题是「逻辑相同、类型不同」的代码复用。没有泛型的世界只有两条烂路:为每种类型写一遍,或者统统用 Any 再强转。泛型让类型本身成为参数:
kotlin
fun <T> firstOrNull(items: List<T>): T? = items.firstOrNull()
val s: String? = firstOrNull(listOf("a", "b"))
val n: Int? = firstOrNull(listOf(1, 2, 3))
Kotlin 标准库里最常见的泛型身影就是集合:List<String>、Map<String, List<Int>>。泛型保证你从 listOf("a") 里取出来的一定是 String,不用强转也不会塞进一个 Int。
六、泛型上界:给 T 划定能力范围
光有 <T> 还不够。假如你要写一个「求列表最大值」的函数,T 必须能比较大小------这时候就需要上界约束:
kotlin
fun <T : Comparable<T>> maxOf(list: List<T>): T {
return list.reduce { acc, item -> if (item > acc) item else acc }
}
maxOf(listOf(3, 1, 2)) // 3,Int 实现了 Comparable<Int>
maxOf(listOf("pear", "apple")) // pear
<T : Comparable<T>> 的意思是:T 必须是 Comparable<T> 的子类型 。这样 item > acc 这个比较操作才合法------编译器知道 T 具备比较能力。如果写 <T : Comparable<T>> 之外的裸 <T>,> 比较会直接编译失败。
去掉上界的后果很直观:所有能力都退化为 Any? 级别的方法,连 compareTo 都调不了,泛型函数里只剩 equals 和 hashcode 可用。所以上界的本质是向编译器声明「我对 T 的最低能力要求」。
多个约束用 where 子句:
kotlin
fun <T> serialize(item: T): String
where T : Comparable<T>, T : java.io.Serializable = item.toString()
ArkTS 对应写法是 function maxOf<T extends Comparable<T>>(list: T[]): T,extends 关键词与 Kotlin 的 : 语义一致。
七、实操演练:一行判空 + 泛型工具
综合练习一个函数:拼接用户显示名(主名 + 昵称),要求不出现 !!、不出现 if-null 命令式判断:
kotlin
fun displayLabel(name: String?, nickname: String?): String {
val label = listOfNotNull(name, nickname).joinToString(" · ")
return label.ifEmpty { "匿名用户" }
}
displayLabel("张三", "三哥") // 张三 · 三哥
displayLabel("张三", null) // 张三
displayLabel(null, null) // 匿名用户
listOfNotNull 自动过滤 null 元素,配合 joinToString 和 ifEmpty,四行代码覆盖所有组合且无一处不安全调用------这就是「用标准库 + 空安全工具替代命令式判空」的典型品味。
再封装一个泛型 + 上界的通用工具:
kotlin
fun <T : Comparable<T>> List<T>.sortedDesc(): List<T> = sortedDescending()
listOf(5, 2, 8).sortedDesc() // [8, 5, 2]
八、小结
- Kotlin 用类型系统区分可空/不可空:
String永不为 null,String?才可以,NPE 被挡在编译期。 - 三件套分工:
?.安全调用(null 传播)、?:Elvis 兜底(null 替代)、!!断言(逃生门,慎用)。 - 与 ArkTS 对照:
?.一致,?:对应??,ArkTS 有 null/undefined 双空,Kotlin 判空后智能转型更省心。 - 泛型让类型成为参数;上界约束
<T : 上界>声明 T 的最低能力,去掉上界 T 就只剩 Any 级别的能力。 - 实战品味:优先
listOfNotNull/?./?:组合替代命令式判空,!!的出现基本等于设计信号。
下一篇进入 W3 的重头戏:Flow 基础与响应式数据流。