Kotlin 类型与空安全:从接口数据到页面状态

Android 页面从 Intent 读取商品 ID,再把接口返回的价格和标题显示出来:ID 可能缺失,价格单位可能是"分",标题可能来自没有可空标注的 Java SDK。把这些值都交给类型推断,编译器也无法替业务判断"缺失"和"无效"是否一样。先用类型表达数据范围,再在外部边界收敛 null 和无效值,页面里的计算才有可靠前提。

本文以 Kotlin 2.4.21 为语言基线。Intent、资源和生命周期属于 Android API,行为还取决于设备 API 级别及所用 AndroidX 版本;下面的纯 Kotlin 示例只说明语言规则。

数值类型先回答"能装什么",再回答"代表什么"

整数有 Byte、Short、Int、Long,浮点数有 Float、Double。没有其他约束时,整数和带小数点的字面量通常推断为 Int、Double;1L 和 1.0f 分别指定 Long、Float。val small: Byte = 127 能编译,是编译器按预期类型检查字面量 的范围;已有的 Int 变量不能这样自动交给 Byte 或 Long。

kotlin 复制代码
fun discountPrice(priceCents: Long, discountCents: Int): Long {
    require(priceCents >= 0 && discountCents >= 0)
    return priceCents - discountCents.toLong()
}

val half = 5.toDouble() / 2   // 2.5;5 / 2 则是 2
val tooLarge: Byte = 128.toByte() // -128,显式转换仍会丢失高位

这里用 Long 保存以分为单位的价格,避免把金额直接交给二进制浮点数累计。toLong() 说明转换发生的位置,却不负责校验业务范围;例子也没有阻止折后价变成负数,生产代码应按折扣规则继续验证。Int 加法溢出不会自动升级为 Long,计算下载字节数等累计值时,应让输入从一开始就采用合适类型。浮点数转整数的 toInt() 会截去小数部分,显示时要另选明确的舍入规则。

这也解释了为什么类型和值域要分开审查:Long 足够装下单笔金额,并不保证两笔金额相加后仍在范围内;Float 符合某个 Android 绘制 API 的参数类型,也不代表它适合持久化业务价格。若服务端传来字符串形式的数量,用 toLongOrNull() 先处理格式错误,再检查业务上下限。把解析异常放到页面渲染时才发现,会使"格式不对"和"页面没显示"混成一个问题。

文本与状态不能混成数字

Boolean 只有 true、false,协议里的 "0"、"1" 得由数据层解释;Char 是一个 UTF-16 代码单元,可用 .code 取其数值,却不能直接当整数参与算术。String 不可变,模板中的 $name 插入变量、${name.length} 计算表达式。Android 用户可见文字仍应走字符串资源,模板不会自动提供本地化或复数规则。

String.length 也按 UTF-16 代码单元计数,表情符号、组合字符让它与用户看到的"字数"不同。协议若按字节限制长度,应先明确字符集再编码;界面若按可见字符限制输入,要按产品定义选择 Unicode 处理方式。循环构造大段文本时可考虑 buildString,别把反复 + 拼接当成免费的操作。

UByte、UShort、UInt、ULong 适合协议位字段、文件格式等明确要求无符号范围的地方。它们仍有固定位宽,运算仍可能溢出。Java、Android API 和存储层常用有符号整数,跨边界要核对数值范围与位模式;普通页码或人数用 Int 加校验通常更直接。

尤其别用无符号数替代输入校验。把负的 Int 转为 UInt,得到的是同一位模式对应的很大正数,不是"拒绝负值"。如果字段约定是零到某个上限,先验证原始输入,再转换为协议需要的类型。反过来,从无符号值传给接受 Int 的 Android 或 Java API,也要确认对方把它理解成数值还是位标志。

null 进入类型之后,分支才看得见

String 不接受 null,String? 才接受。安全调用 ?. 在接收者为 null 时停止当前调用链,Elvis 运算符 ?: 在左侧为 null 时给出备用值。它不把空白字符串自动判成缺失,这需要业务规则明确写出来。

kotlin 复制代码
fun displayName(raw: String?): String =
    raw?.trim()?.takeIf { it.isNotEmpty() } ?: "访客"

fun requireProductId(raw: String?): String {
    val id = raw?.trim()?.takeIf(String::isNotEmpty)
        ?: error("缺少商品 ID")
    return id // 此处已是非空 String
}

"访客"是允许缺失时的默认展示;商品 ID 缺失则不能继续请求,error() 返回 Nothing,表示该分支不会正常交出结果。?: 右侧也可以直接 return。需要读取可空值后继续调用时,先做 if (value == null) return,编译器可在剩余路径把稳定的局部值智能转换为非空。可变属性或可覆写 getter 可能在两次读取间变化,先复制到局部 val 更容易证明安全。

Android 项目的缺口常在 Java 互操作处:缺少可信可空信息的 Java 返回值会成为平台类型,Kotlin 无法保证它非空。Bundle、游标、第三方 SDK 的结果进入业务层时,尽早声明成 String? 并校验,或者验证后收敛为 String。!! 遇到 null 会抛异常,不会"修复"平台类型;只有已经建立且愿意强制检查的约束才适合用它。lateinit 也只是推迟非空 var 的赋值,读取前未初始化仍会失败。

平台类型通常在 IDE 中写作 String! 一类形式,源码里不能把 ! 当普通类型标注来声明变量。若 Java 方法宣称非空而运行时交出 null,注解和编译检查也不能替代边界验证。面对这种值,先复制到局部变量并写出可空契约;这一步还把不稳定的重复读取收敛成一次读取。对于 Intent extra 等 Android API,返回值的可空约束应按实际方法签名和目标 API 检查,别凭旧版本的印象加 !!。

Any、Unit、Nothing 和数组各管一件事

Any 是非空值的公共上界;若 null 也合法,写 Any?。Unit 表示函数正常完成而没有业务返回值,回调类型 (Event) -> Unit 中常见。Nothing 表示没有正常返回路径,例如始终抛异常的函数;它解释了上面 error() 为什么能出现在需要 String 的表达式分支。三者不是可以互换的"空返回值"。

kotlin 复制代码
fun record(value: Any): Unit = println(value)
fun reject(reason: String): Nothing = error(reason)

val labels: Array<String> = arrayOf("首页", "详情")
val pixels: IntArray = intArrayOf(0xff, 0x80)
labels[0] = "列表"
record(pixels.sum())

Array<T> 长度固定、元素可改;IntArray 等专用数组与 Array<Int> 类型不同,在 JVM 上可避免逐个整数装箱。数组是不变型的:Array<String> 不能作为 Array<Any> 传入,否则接收方就可能写入非字符串。copyOf() 只复制容器,可变元素仍被两份数组共享。Android 图像缓冲和 Java API 常需要数组,业务列表则先看集合是否更合适。

数组与列表也不能只凭 [] 外观判断。List<T> 在 Kotlin 类型上提供只读访问,但若底层仍被其他持有者修改,它不是不可变数据的承诺。需要给异步任务稳定快照时,应按元素是否可变决定复制容器还是进一步复制对象。Array<T> 若要传给接收可变数组的 Java 方法,还需问清它会不会原地写入:固定长度不等于内容安全。

回到页面入口,检查顺序可以很朴素:接口字段的数值范围和单位是什么,文本如何编码与展示,缺失值是默认显示还是中止流程,进入内部代码后是否已收敛成明确类型。类型系统会守住写进签名的约束;签名之外的金额规则、字符规则和生命周期,仍要由调用方说明。

参考资料:Kotlin 基本类型、空安全、无符号整数、数组。

相关推荐
墨天梦14 小时前
D19_系统分享与文件URI
gitee·kotlin
应用市场18 小时前
离线小游戏做“每日挑战”:日期当随机种子,整局存成 JSON,再加一个“我不是机器人”
android·kotlin·游戏开发
hai_android1 天前
LiveEventBusCore 跨 Activity 通讯源码解析
android·kotlin·android jetpack
维克兜率天1 天前
【维克】统计套利建模:从因子到信号的完整流程
android·开发语言·python·算法·kotlin
释厄6232 天前
错误无误公理——错误正确本同源都迭代定律——逆转有价值
android·开发语言·kotlin
程序猿追2 天前
立方体转到九十度:用 kotlin-math 核对鸿蒙上的两种旋转
android·开发语言·kotlin
程序猿追2 天前
kotlin-retry 接入鸿蒙后,怎样确认它真的会停止重试
开发语言·kotlin·harmonyos
应用市场3 天前
不用一张图片的像素小游戏:小人是字符串,房间是矩形,平衡靠两千个假玩家
android·kotlin·游戏开发