KMP学习day4--Kotlin空安全与泛型

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 基础与响应式数据流。

相关推荐
東隅已逝,桑榆非晚17 分钟前
算法6. 双指针法-查找总价格为目标的两个商品
c++·学习·算法
wdyan2971 小时前
带着小白学习-微服务-5 实战:配置中心-数据库连接
数据库·学习·微服务
叼烟扛炮2 小时前
Linux 学习笔记(五十):网络层——IP 协议、子网划分与 NAT 魔法
linux·笔记·学习
叼烟扛炮2 小时前
Linux 学习笔记(六十):单线程的逆袭——epoll 高并发服务器实战
linux·服务器·网络·笔记·学习·算法
Rabitebla3 小时前
【Linux操作系统】进程篇(十一):进程程序替换——从底层原理到 exec 函数族
linux·服务器·c++·笔记·学习·算法
叼烟扛炮4 小时前
[LangChain] 学习笔记(03):打破失忆症——对话机制与上下文管理
java·前端·javascript·笔记·学习·langchain
旖旎夜光4 小时前
【C++ 基础入门】类和对象(下):初始化列表、static 成员、友元与内部类
c++·笔记·学习
LONGZHIQIN5 小时前
STM32学习笔记
笔记·stm32·学习·arm
redtank20005 小时前
纯血鸿蒙小工具三件套实测:弹幕手持、爱记录与仓库管理软件
学习·华为·智能手机·harmonyos