Kotlin 多美元符号插值实战:告别 ${'$'} 转义地狱的优雅方案
你是否在 Kotlin 中写过包含大量 $ 符号的字符串?JSON Schema、正则表达式、Shell 脚本模板、SQL 语句……这些场景里的美元符号无处不在,而 Kotlin 的字符串插值恰好也以 $ 作为触发符。于是,一个令人头疼的问题出现了:你不得不用 ${'$'} 逐个转义每个字面量美元符号,代码变得支离破碎。Kotlin 2.1.0 引入的 多美元符号字符串插值(Multi-Dollar String Interpolation) 特性,正是为了解决这个痛点。
痛点:${'$'} 转义地狱
先看一个真实场景——用 Kotlin 生成 JSON Schema:
// Kotlin 2.1.0 之前的写法
val jsonSchema = """
{
"${'$'}schema": "https://json-schema.org/draft/2020-12/schema",
"${'$'}id": "https://example.com/product.schema.json",
"${'$'}dynamicAnchor": "meta",
"title": "${simpleName ?: "unknown"}",
"type": "object"
}
"""
每个 JSON Schema 的关键词($schema、$id、$dynamicAnchor)都需要用 ${'$'} 来转义,否则 Kotlin 编译器会把它们当成字符串插值。三个 $ 关键词还算能忍受,但如果模板里有十几个甚至几十个美元符号呢?
再看一个正则表达式的例子:
// 匹配价格格式:$12.99、$100.00
val pricePattern = """\${'$'}(\d+)\.(\d{2})"""
// 实际想要的正则:\$(\d+)\.(\d{2})
还有 Shell 脚本模板:
val shellScript = """
#!/bin/bash
echo "Total: ${'$'}TOTAL"
echo "Count: ${'$'}COUNT"
for item in "${'$'}@"; do
echo "Processing: ${'$'}item"
done
""".trimIndent()
每个 $ 都要写成 ${'$'},代码的可读性急剧下降,维护起来更是噩梦。
新语法:多美元符号插值
Kotlin 2.1.0 引入了 多美元符号字符串插值,核心思想很简单:你可以指定需要多少个连续的 $ 才触发插值,少于这个数量的 $ 会被当作普通字面字符。
基本语法
在字符串前加上多个 $ 作为前缀,就能设定插值的触发阈值:
val name = "Kotlin"
// $$ 前缀:需要两个连续 $ 才触发插值,单个 $ 是字面量
val greeting = $$"""
Hello $name, welcome!
Price: $9.99
Interpolation: $$name
"""
println(greeting)
// 输出:
// Hello $name, welcome! ← 单个 $ 是字面量,不插值
// Price: $9.99 ← 字面量
// Interpolation: Kotlin ← 两个 $$ 触发插值
关键规则:
$$前缀表示 两个连续$才触发插值- 单个
$保持为字面字符,无需转义 - 要插值变量,必须写
$$variableName或$${expression}
三级插值
你甚至可以使用三个 $,实现更精细的控制:
val product = "carrot"
val data = $$$"""
{
"currency": "$",
"amount": "42.45 $$",
"$$serviceField": "none",
"product": "$$$product"
}
"""
println(data)
// 输出:
// {
// "currency": "$", ← 单个 $ 是字面量
// "amount": "42.45 $$", ← 两个 $$ 也是字面量
// "$$serviceField": "none", ← 两个 $$ 也是字面量
// "product": "carrot" ← 三个 $$$ 触发插值
// }
启用方式
这个特性目前是 实验性的(Experimental),需要手动开启。
方式一:命令行编译
kotlinc -Xmulti-dollar-interpolation main.kt
方式二:Gradle 配置
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xmulti-dollar-interpolation")
}
}
实战场景
场景 1:JSON Schema 生成
回到开头的 JSON Schema 例子,用多美元符号插值重写:
val simpleName = "Product"
val jsonSchema = $$"""
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"$dynamicAnchor": "meta",
"title": "$${simpleName ?: "unknown"}",
"type": "object",
"properties": {
"name": { "type": "string" },
"price": { "$ref": "#/definitions/Price" }
},
"definitions": {
"Price": {
"type": "number",
"minimum": 0,
"description": "Price in USD ($)"
}
}
}
"""
对比一下:
$schema、$id、$dynamicAnchor、$ref直接写,不需要转义- 需要插值的地方用
$$触发,比如$${simpleName ?: "unknown"} - 代码清爽了许多,JSON Schema 的结构一目了然
场景 2:正则表达式模板
// 匹配各种货币格式的价格
val currencySymbol = "¥"
val priceRegex = $$"""
(?<symbol>[$€£¥]) # 货币符号(字面量 $ 不需要转义)
(?<amount>\d+) # 金额
\.(?<cents>\d{2}) # 分
# 默认货币: $${currencySymbol}
"""
正则表达式中 $ 是常见的元字符(表示行尾、反向引用等),多美元符号插值让你不再需要为每个 $ 写 ${'$'}。
场景 3:Shell 脚本模板
val appName = "myapp"
val logDir = "/var/log/$appName"
val script = $$"""
#!/bin/bash
set -euo pipefail
APP_NAME="$${appName}"
LOG_DIR="$${logDir}"
echo "Starting ${'$'}APP_NAME..."
mkdir -p "${'$'}LOG_DIR"
for arg in "${'$'}@"; do
echo "Arg: ${'$'}arg"
done
TOTAL=${'$'}(find "${'$'}LOG_DIR" -type f | wc -l)
echo "Found ${'$'}TOTAL log files"
"""
用 $$ 前缀后,Shell 中的 $APP_NAME、$LOG_DIR、$@、$TOTAL 等变量引用都可以直接写,只有需要 Kotlin 插值的地方才用 $$。
场景 4:Mustache / Handlebars 模板
val title = "用户列表"
val template = $$"""
<h1>$${title}</h1>
<ul>
{{#each users}}
<li>$${this.name} - $${this.email}</li>
{{/each}}
</ul>
<p>共 {{users.length}} 位用户</p>
<script>
const data = $${userJson};
// 模板中的 $ 符号不需要转义
const price = "$9.99";
</script>
"""
模板引擎的 {{}} 语法和 JavaScript 的 $ 符号都不会被 Kotlin 误解析。
向后兼容性
多美元符号插值 不会影响现有代码。如果你不使用 $$ 或 $$$ 前缀,字符串的行为和之前完全一致:
val name = "Kotlin"
// 普通字符串:行为不变
val s1 = "Hello $name" // "Hello Kotlin"
val s2 = """Price: ${'$'}9.99""" // "Price: $9.99"
// 多美元前缀字符串:新行为
val s3 = $$"""Hello $name""" // "Hello $name"($ 是字面量)
val s4 = $$"""Hello $$name""" // "Hello Kotlin"($$ 触发插值)
这意味着你可以逐步迁移,不需要一次性改写所有字符串。
最佳实践
1. 根据场景选择合适的 $ 数量
| 场景 | 推荐前缀 | 理由 |
|---|---|---|
| JSON Schema | $$ | $schema 等关键词用单个 $ |
| 正则表达式 | $$ | $ 作为行尾锚点很常见 |
| Shell 脚本 | $$ | Shell 变量用单个 $ |
| 混合模板 | $$$ | 需要同时包含 $ 和 $$ 字面量 |
2. 保持团队约定一致
在项目中统一使用一种风格,避免有些地方用 ${'$'} 转义,有些地方用多美元前缀:
// 团队约定示例:所有包含 $ 字面量的字符串统一使用 $$ 前缀
// build.gradle.kts 中全局启用 -Xmulti-dollar-interpolation
3. 单行字符串也支持
多美元插值不仅适用于多行字符串("""),也适用于单行字符串:
val price = $$"Cost: $9.99" // "Cost: $9.99"
val total = $$"Sum: $${a + b}" // 触发插值
总结
多美元符号字符串插值是 Kotlin 2.1.0 引入的一项实用特性,它让你可以控制字符串插值的触发条件,从而在包含大量 $ 符号的场景中告别繁琐的 ${'$'} 转义。核心收益:
- 可读性提升:JSON Schema、正则、Shell 模板中的
$符号直接书写 - 维护性增强:不再需要逐个转义,减少出错概率
- 向后兼容:不影响现有代码,可按需逐步采用
- 灵活控制:
$$、$$$前缀提供多级插值粒度
如果你的项目已经升级到 Kotlin 2.1.0+,不妨在 build.gradle.kts 中启用这个特性,下次写模板字符串时体验一下代码从"转义地狱"到"清爽自然"的转变。