跳到主要内容

Kotlin 2.4 Map fallback 新 API 实战:用 getOrPutIfNull 和 getOrPutIfMissing 告别手动判空

· 阅读需 6 分钟
重庆 Kotlin User Group

在处理配置、缓存或数据映射时,你一定遇到过这样的场景:从 Map 中获取值,如果不存在就提供默认值。Kotlin 的 getOrDefault() 和 getOrPut() 已经很实用了,但它们都有一个盲区——无法区分"键不存在"和"值为 null"。

来看一个真实的例子:

val config = mutableMapOf<String, String?>(
"username" to "alice",
"theme" to null // 用户明确设置了 theme 为 null
)

// 问题:getOrDefault 无法区分"键不存在"和"值为 null"
val theme1 = config.getOrDefault("theme", "light")
println(theme1) // 输出: light ❌ 错误!用户明确设置了 null,应该保留 null

val theme2 = config.getOrDefault("language", "zh-CN")
println(theme2) // 输出: zh-CN ✅ 正确,键不存在时使用默认值

这段代码的问题在于:getOrDefault() 在键存在但值为 null 时,也会返回默认值。但很多时候,我们需要区分这两种情况:

  • 键不存在:使用默认值
  • 键存在但值为 null:保留 null(用户明确设置)

传统方案:手动检查​

在 Kotlin 2.4 之前,你需要手动检查键是否存在:

val theme = if (config.containsKey("theme")) {
config["theme"] // 可能是 null
} else {
"light" // 键不存在时使用默认值
}

这种写法虽然正确,但太冗长了。每次都要写 containsKey() 检查,代码可读性很差。

Kotlin 2.4 的解决方案​

Kotlin 2.4 引入了两个实验性 Map 扩展函数,完美解决了这个问题:

1. getOrPutIfNull():键不存在或值为 null 时使用默认值​

val config = mutableMapOf<String, String?>(
"username" to "alice",
"theme" to null
)

// 键存在但值为 null → 替换为默认值
val theme = config.getOrPutIfNull("theme") { "light" }
println(theme) // 输出: light
println(config) // 输出: {username=alice, theme=light}

// 键不存在 → 使用默认值
val language = config.getOrPutIfNull("language") { "zh-CN" }
println(language) // 输出: zh-CN
println(config) // 输出: {username=alice, theme=light, language=zh-CN}

// 键存在且值非 null → 保留原值
val username = config.getOrPutIfNull("username") { "guest" }
println(username) // 输出: alice(保持不变)

适用场景:当你希望"null 和不存在都视为需要默认值"时使用。

2. getOrPutIfMissing():仅在键不存在时使用默认值​

val config = mutableMapOf<String, String?>(
"username" to "alice",
"theme" to null
)

// 键存在但值为 null → 保留 null(不替换)
val theme = config.getOrPutIfMissing("theme") { "light" }
println(theme) // 输出: null
println(config) // 输出: {username=alice, theme=null}

// 键不存在 → 使用默认值
val language = config.getOrPutIfMissing("language") { "zh-CN" }
println(language) // 输出: zh-CN
println(config) // 输出: {username=alice, theme=null, language=zh-CN}

适用场景:当你需要严格区分"键不存在"和"值为 null"时使用。

对比总结​

方法键不存在键存在但值为 null键存在且值非 null
getOrDefault(key, default)返回 default返回 default ❌返回原值
getOrPut(key) { default }返回 default返回 default ❌返回原值
getOrPutIfNull(key) { default }返回 default返回 default ✅返回原值
getOrPutIfMissing(key) { default }返回 default返回 null ✅返回原值

关键区别:

  • getOrPutIfNull() 把"键不存在"和"值为 null"视为等价
  • getOrPutIfMissing() 严格区分两者

实战场景​

场景 1:用户配置管理​

data class UserConfig(
val settings: MutableMap<String, String?>
) {
fun getTheme(): String {
// 用户明确设置 null 表示"使用系统主题"
// 但这里我们希望 null 也回退到默认主题
return settings.getOrPutIfNull("theme") { "system" }!!
}

fun getNotificationEmail(): String? {
// 用户明确设置 null 表示"不接收邮件通知"
// 必须保留 null,不能替换为默认值
return settings.getOrPutIfMissing("notification_email") {
"default@example.com"
}
}
}

场景 2:缓存与数据加载​

class DataRepository {
private val cache = mutableMapOf<String, Data?>()

fun getData(key: String): Data {
// 缓存命中(即使是 null)就返回
// 缓存未命中才加载数据
return cache.getOrPutIfMissing(key) {
loadDataFromDatabase(key)
} ?: throw DataNotFoundException(key)
}

fun getDataOrNull(key: String): Data? {
// null 表示"数据不存在",需要加载
// 加载后即使是 null 也缓存起来(避免重复查询)
return cache.getOrPutIfNull(key) {
loadDataFromDatabase(key)
}
}
}

场景 3:配置文件解析​

fun parseConfig(rawConfig: Map<String, String?>): AppConfig {
val config = rawConfig.toMutableMap()

return AppConfig(
// 必填项:不存在或 null 都使用默认值
host = config.getOrPutIfNull("host") { "localhost" }!!,
port = config.getOrPutIfNull("port") { "8080" }!!.toInt(),

// 可选项:保留用户明确设置的 null
proxy = config.getOrPutIfMissing("proxy") { "default-proxy" },

// 调试模式:null 表示"禁用",不能替换为默认值
debugMode = config.getOrPutIfMissing("debug") { "false" }!!.toBoolean()
)
}

如何启用​

这两个函数在 Kotlin 2.4 中是实验性特性,需要手动开启。

Gradle(Kotlin DSL):

kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.ExperimentalStdlibApi")
}
}

代码中使用:

@OptIn(ExperimentalStdlibApi::class)
fun processConfig(config: MutableMap<String, String?>) {
val value = config.getOrPutIfNull("key") { "default" }
}

或者在文件顶部添加:

@file:OptIn(ExperimentalStdlibApi::class)

package com.example

fun processConfig(config: MutableMap<String, String?>) {
val value = config.getOrPutIfNull("key") { "default" }
}

实现原理​

这两个函数的实现非常简洁,核心逻辑就是先检查键是否存在:

// 简化版实现
@ExperimentalStdlibApi
public inline fun <K, V> MutableMap<K, V>.getOrPutIfNull(
key: K,
defaultValue: () -> V
): V {
val existingValue = this[key]
return if (existingValue == null && !this.containsKey(key)) {
val default = defaultValue()
this[key] = default
default
} else if (existingValue == null) {
// 键存在但值为 null,替换为默认值
val default = defaultValue()
this[key] = default
default
} else {
existingValue
}
}

@ExperimentalStdlibApi
public inline fun <K, V> MutableMap<K, V>.getOrPutIfMissing(
key: K,
defaultValue: () -> V
): V? {
return if (this.containsKey(key)) {
this[key] // 可能返回 null
} else {
val default = defaultValue()
this[key] = default
default
}
}

注意 getOrPutIfMissing() 的返回类型是 V?,因为它可能返回 null。

与现有 API 的对比​

Kotlin 标准库已经有一些 Map 相关的便捷函数:

val map = mutableMapOf<String, String>()

// 已稳定的 API
map.getOrDefault("key", "default") // 键不存在时返回默认值
map.getOrPut("key") { "default" } // 键不存在时计算并放入默认值

// 新增实验性 API
map.getOrPutIfNull("key") { "default" } // 键不存在或值为 null 时
map.getOrPutIfMissing("key") { "default" } // 仅键不存在时

选择建议:

  • 如果你的 Map 值类型是非空类型(Map<String, String>),继续使用 getOrPut()
  • 如果你的 Map 值类型是可空类型(Map<String, String?>),根据语义选择:
    • 需要区分 null 和不存在 → getOrPutIfMissing()
    • 不需要区分 → getOrPutIfNull()

总结​

Kotlin 2.4 的 getOrPutIfNull() 和 getOrPutIfMissing() 填补了标准库在处理可空值 Map 时的空白:

  1. 更精确的语义:明确区分"键不存在"和"值为 null"
  2. 更少的样板代码:告别手动 containsKey() 检查
  3. 更好的可读性:函数名直接表达意图

虽然还是实验性特性,但在配置管理、缓存、数据映射等场景中已经非常实用。如果你经常处理 Map<String, T?> 类型的代码,不妨试试这两个新 API。