1. 概述

本文将深入讲解 Kotlin 中的字符串模板(String Templates),包括其基本用法、高级特性以及在多行字符串中处理 $ 符号的常见踩坑场景。

如果你还不熟悉 Kotlin 的基础语法或想系统学习,可以参考我们的 Kotlin 入门教程

2. Kotlin 中的字符串基础

与 Java 一样,Kotlin 的 String 是不可变的。一旦创建,就不能修改其内容,但可以通过操作生成新的字符串实例。

Kotlin 在 JVM 的 String 类基础上扩展了大量实用方法,显著提升了开发效率。

✅ 示例:使用 padEnd() 方法补全字符串长度:

"Hello".padEnd(10, '!')

执行结果为 "Hello!!!!!" —— 将原字符串右侧填充至总长 10,不足部分用 ! 补齐。

这类增强功能让字符串格式化更直观,无需依赖 StringBuilder 或第三方库。

3. 字符串模板详解

字符串模板是指包含嵌入式表达式的字符串字面量,它允许你在双引号内直接插入变量或表达式,实现类似“插值”的效果。

基本语法

对比 Java 写法:

String message = "n = " + n;

Kotlin 更简洁:

val message = "n = $n"

👉 只需在变量名前加 $,即可完成自动拼接。

支持任意 Kotlin 表达式

通过 ${} 语法,可嵌入任意合法的 Kotlin 表达式:

val message = "n + 1 = ${n + 1}"

甚至支持条件逻辑:

val message = "$n is ${if (n > 0) "positive" else "not positive"}"

⚠️ 注意:虽然 Kotlin 中很多结构是表达式(如 ifwhen),但并非全部都适用。此处能正常工作是因为 if 在 Kotlin 中本身就是表达式,会返回一个值。

由于 ${} 内部是完整表达式上下文,因此里面的双引号不需要转义 —— 它们属于另一个字符串字面量。

模板解析机制

字符串模板的求值过程如下:

  1. 计算 ${} 中的表达式;
  2. 调用结果对象的 toString() 方法;
  3. 将结果插入原位置。

模板嵌套(不推荐)

理论上支持深度嵌套:

val message = "$n is ${if (n > 0) "positive" else 
  if (n < 0) "negative and ${if (n % 2 == 0) "even" else "odd"}" else "zero"}"

但这种写法可读性差,容易出错 ❌。建议将复杂逻辑提取到函数中,保持模板简洁 ✅。

如何输出原始 $ 符号?

若要显示字面意义上的 $ 而非触发模板替换,需进行转义:

val message = "n = \$n"

此时 $n 不再被识别为变量引用,而是普通文本。

4. 原始字符串(Raw Strings)

Kotlin 提供三重引号 """...""" 定义的原始字符串,适用于多行文本和含特殊字符的内容,无需转义反斜杠等符号。

多行文本示例

Java 中定义 Windows 路径需要双重转义:

String path = "C:\\Repository\\read.me";

Kotlin 使用原始字符串更清晰:

val path = """C:\Repository\read.me"""

多行字符串与缩进控制

可用于构建结构化文本,例如账单信息:

val receipt = """Item 1: $1.00
Item 2: $0.50"""

该字符串精确保留换行,生成两行内容。

若希望代码美观并统一缩进,可用 trimMargin()

val receipt = """Item 1: $1.00
                >Item 2: $0.50""".trimMargin(">")

trimMargin(">") 会移除每行开头到第一个 > 为止的所有空白字符,最终输出仍保持整洁。

原始字符串中的限制

原始字符串不支持任何转义序列。例如:

val receipt = """Item 1: $1.00\nItem 2: $0.50"""

结果不会换行,而是原样输出 \n 两个字符。

但支持模板功能!

尽管不能使用 \n,但原始字符串依然支持字符串模板:

val receipt = """Item 1: $1.00${"\n"}Item 2: $0.50"""

👉 利用 ${} 插入真正的换行符 \n,巧妙绕过限制。

5. 多行字符串中使用美元符号($)的解决方案

当你的字符串包含 $ 且不属于 Kotlin 变量时(如 MongoDB 查询),很容易踩坑。

5.1 问题背景

假设我们在 Kotlin 应用中构造 MongoDB 查询语句,筛选年龄 ≥18 的用户:

db.people.find(
  {
    "age": { $gte: 18 }
  }
)

尝试直接放入原始字符串:

val findAdultsQuery = """
db.people.find(
  {
    "age": { $gte: 18 }
  }
)
"""

❌ 编译失败!Kotlin 试图将 $gte 解释为变量,但未定义。

尝试转义:

val findAdultsQuery = """
db.people.find(
  {
    "age": { \$gte: 18 }
  }
)
"""

❌ 仍然报错!因为原始字符串 不支持反斜杠转义特殊字符

5.2 推荐方案:使用 ${'$'} 插值

✅ 最佳实践是利用字符串模板插入字面量 $

val dollarChar = "$"
val findAdultsQuery = """
db.people.find(
  {
    "age": { ${'$'}gte: 18 }
  }
)
"""
assertTrue(dollarChar in findAdultsQuery)

✔️ 成功生成包含 $gte 的正确 JSON 查询。

💡 原理:${'$'} 是一个表达式,返回字符 $,然后作为字符串拼入。

此方法也适用于带 \n 的双引号多行字符串:

val dollarChar = "$"
val findAdultsQuery = "db.people.find(\n{\n\"age\": { ${'$'}gte: 18 }\n }\n)"
assertTrue(dollarChar in findAdultsQuery)

5.3 替代方案:使用双引号 + 转义

如果对格式要求不高,也可改用双引号字符串,并用 \ 转义 $

val dollarChar = "$"
val findAdultsQuery = "db.people.find(\n{\n\"age\": { \$gte: 18 }\n }\n)"
assertTrue(dollarChar in findAdultsQuery)

✅ 此方式有效,但牺牲了可读性,尤其在复杂 JSON 场景下维护困难。

📌 总结建议:

  • 日常优先使用 """...""" 配合 ${'$'}
  • 简单场景可用双引号转义;
  • 避免混合过多逻辑在字符串内,必要时封装成函数。

6. 总结

Kotlin 的字符串模板是一项强大且 Java 所不具备的功能,极大简化了字符串拼接和动态内容生成。

我们重点探讨了:

  • 模板语法 $variable${expression}
  • 原始字符串 """...""" 的优势与限制
  • 在 MongoDB 等 DSL 场景中安全使用 $ 的最佳实践:${'$'}

所有示例代码均可在 GitHub 获取:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-strings


原始标题:Kotlin String Templates