1. 多行字符串
当你刚开始编程时,控制台输出似乎很简单:只需调用 println("内容") 即可。 但一旦你想打印菜单、帮助文本、一个“精美的页眉”或跨越多行的简要报告时,麻烦就来了。 你可以手动插入 \n,但代码会变得难以阅读,修改文本也会变成一场“寻找多余斜杠在哪里”的游戏。
这正是 Kotlin 引入多行字符串(也称为 原始字符串/raw strings)的原因:它们让你在代码中编写文本时,格式几乎与在控制台中显示的一模一样,无需再受 \n 之苦。 Kotlin 规范明确建议使用多行字符串,而不是手动嵌入 \n,并使用 trimIndent() 或 trimMargin() 来控制缩进。
作为热身,让我们看一个对比。首先是比较笨拙的 \n 方式,然后是更优雅的方式。
fun main() {
val help = "Commands:\n" +
"1) add\n" +
"2) list\n" +
"3) exit\n"
print(help)
}
是的,它能工作。但修改这样的代码简直是一种折磨,难度不亚于“用牙刷清理键盘”。
多行字符串语法 """...""" 及其与 "..." 的区别
Kotlin 中的多行字符串使用三个双引号编写:""" ... """。 这看起来有点不寻常,但逻辑很简单:在这样的字面量内部,你可以直接换行。 也就是说,你按下 Enter 键,字符串中就会出现一个真实的换行符。 这极大地提高了可读性,尤其是对于大块文本:菜单、报告模板、“横幅”等。
一个重要的细微差别:它被称为 raw(原始) 并不是因为它没写完,而是因为在它内部几乎不需要转义任何内容。 特别是,原始字符串内部的 \n 和 \t 只是两个普通的字符:\ 和 n(或 t),而不是“换行”和“制表符”。 这既方便又微妙:方便是因为减少了转义;微妙是因为有时你期望得到某种结果,实际却得到了另一种。
这是一个多行字符串的基础示例:
fun main() {
val help = """
Commands:
1) add
2) list
3) exit
"""
print(help)
}
请注意:控制台中的文本将与代码中的完全一致。这就是核心理念。
为什么缩进会“粘”在输出中,以及为什么需要 trimIndent()
当你在函数内部开始编写多行文本时,通常会在代码中进行缩进——仅仅是因为这样看起来整洁且符合格式化规则。 但原始字符串会逐字保留空格。 这导致了一个经典情况:在代码中一切都对齐了,但在控制台中你的菜单突然向右偏移了四个空格,因为你在 fun main() 块内部编写了字符串。
trimIndent() 专门解决了这个问题:它会移除多行字面量中所有非空行共有的(最小)缩进。 Kotlin 规范这样描述: “当结果不需要特殊的内部缩进时,请使用 trimIndent()。”
示例:先看“反面教材”(输出中会出现缩进),再看“正面示例”。
fun main() {
val help = """
Commands:
1) add
2) list
3) exit
"""
print(help)
}
这段代码会打印带有缩进的行。现在看同样的示例,但使用了 trimIndent():
fun main() {
val help = """
Commands:
1) add
2) list
3) exit
""".trimIndent()
print(help)
}
现在,那些为了让源代码好看而存在的空格就不会弄乱最终的文本了。
一个简单的记忆方法:trimIndent() 就像是“移除代码的格式化缩进,但如果文本内部确实存在相对缩进差异,则保留它们”。
trimMargin() — 当你需要更精确的缩进控制时
有时你希望文本内部保留缩进(例如,你正在打印伪代码、电子邮件模板或报告块), 但你又不希望因为 Kotlin 代码格式化而在输出左侧带入多余的“垃圾”空格。 在这种情况下,trimIndent() 可能不够可控:它移除共有缩进,但不允许你显式标记文本边界。
这就是 trimMargin() 发挥作用的地方:你选择一个边界符(默认为 |), 并编写每一行,使得“边界符左侧”的所有内容都被切掉。 在 Kotlin 规范中,这被描述为需要保留重要内部缩进时的首选方案。
使用 trimMargin() 的示例:
fun main() {
val report = """
|User: Ira
|Status: OK
|Message: Data accepted
""".trimMargin()
println(report)
}
在这里,你可以随心所欲地调整代码缩进,但“文本边界”保持稳定。 如果你仔细观察 Kotlin 指南中的示例,你会发现思路是一样的: | 有助于整齐地“修剪”左侧部分,保持输出整洁。
小贴士:trimMargin() 不仅可以使用 |。 你可以选择其他字符(例如 >),但在大多数情况下,| 是事实上的标准,除非你打算开一家专门制造轮子的工厂,否则最好不要重新发明轮子。
多行字符串与字符串模板:如何插值
到目前为止,我们在普通字符串中使用了字符串模板:$name,${a + b} 等等。 它们在 """...""" 内部的工作方式完全相同。 这非常方便:你可以制作一个多行的“报告模板”,并直接插入变量而无需进行字符串拼接。
快速回顾一下模板语法:你可以使用 $a 插入简单标识符,使用 ${...} 插入表达式。
这是一个“多行报告 + 数据”的简短示例:
fun main() {
val user = "Ira"
val total = 3
val text = """
|User report
|---------------------
|Name: $user
|Purchases: $total
""".trimMargin()
println(text)
}
在上一讲中,我们讨论了一个有趣的情况:如果你需要打印作为文本的美元符号,Kotlin 会尝试将 $ 视为模板的开始。 在普通字符串中,你会写 "\$"。 这在原始字符串中也同样重要,但今天我们不再深究——只需记住 $ 是插值的“特殊字符”,它并不总是代表“仅仅是一个美元符号”。
2. 简单文本对齐:padStart() 和 padEnd()
如何在不求助于 Excel 的情况下制作简单的“列”
学会打印多行文本后,你几乎立刻就会产生下一个想法: “我能不能让数字在一列中对齐,就像表格一样?” 当然可以。不,我们不需要 Excel,不需要神奇的格式化程序,也不需要在 1997 年的针式打印机上打印(虽然那也别有一番风味)。
padEnd(width) 在字符串右侧填充空格(或其他字符),直到达到所需的长度。 padStart(width) 则在左侧进行同样的操作。 结果是,你可以左对齐名称,右对齐数字,就像在报告和收据中看到的那样。
一个重要的注意事项:这些函数作用于字符串。 因此,数字 (Int) 通常需要通过 toString() 转换为字符串。这个你已经知道了。
迷你对齐示例:
fun main() {
val item = "Tea"
val qty = 3
val price = 90
val line = item.padEnd(10) +
qty.toString().padStart(3) +
" x " +
price.toString().padStart(4)
println(line) // Tea 3 x 90
}
这里我们制作了一个“非常简单的”类似表格的行。 虽然这还不是一个完整的报告,但你可以看到,即使 qty 变为 12,price 变为 160,数值也不会乱跳。
3. 在控制台应用中打印收据
现在我们将整合一个完整的小程序片段,并在后续课程中继续完善它。 构思很简单:一个控制台里的微型“咖啡店”。用户输入数量(目前暂不涉及复杂命令,也不存储历史记录),程序打印出一张整洁的收据:页眉、列表和总额。
为什么这是本课的一个好例子:我们立刻拥有了多行的“页眉”和“页脚”,以及使用 padStart()/padEnd() 能够大显身手的表格行。
首先,创建一个返回精美页眉的函数。 我们特意返回一个 String,以便让你看到:多行字符串只是一个普通的字符串——只是写起来更方便。
fun buildHeader(): String {
return """
|========================
| Kotlin Coffee CLI
|========================
""".trimMargin()
}
现在让我们编写一个用于格式化单行收据的函数。 目前我们尽量保持简单,直接使用字符串,不添加任何花哨的东西。
fun formatLine(name: String, qty: Int, price: Int): String {
val sum = qty * price
return name.padEnd(12) +
qty.toString().padStart(3) +
" x " +
price.toString().padStart(4) +
" = " +
sum.toString().padStart(5)
}
注意代码风格:我们先计算 sum,然后再格式化该行。 这几乎总是比在长字符串拼接中间塞入 qty * price 更具可读性。 在上一讲中,我们讨论过类似的字符串模板思路:复杂的东西先放进 val,然后再放进文本。
接下来是 main 函数,我们在其中使用页眉、格式化功能和多行输入提示。 这里我们使用数组,因为它们非常适合表示初始的“固定产品集”。
fun main() {
val names = arrayOf("Espresso", "Cappuccino", "Tea")
val prices = intArrayOf(120, 160, 90)
val qty = IntArray(names.size)
println(buildHeader())
for (i in names.indices) {
print("How many '${names[i]}'? ")
qty[i] = readln().toInt()
}
println()
println("Receipt:")
for (i in names.indices) {
println(formatLine(names[i], qty[i], prices[i]))
}
}
让我们添加最后的修饰:同样作为多行字符串的收据页脚,并显示总金额。 总额只是一个 Int;我们在循环中计算它。
fun buildFooter(total: Int): String {
return """
|
|------------------------
|TOTAL: ${total.toString().padStart(5)} EUR.
|Thank you! Come again :)
""".trimMargin()
}
并更新 main 的结尾,添加 total 计算并打印页脚:
fun main() {
val names = arrayOf("Espresso", "Cappuccino", "Tea")
val prices = intArrayOf(120, 160, 90)
val qty = IntArray(names.size)
println(buildHeader())
for (i in names.indices) {
print("How many '${names[i]}'? ")
qty[i] = readln().toInt()
}
var total = 0
println("\nReceipt:")
for (i in names.indices) {
total += qty[i] * prices[i]
println(formatLine(names[i], qty[i], prices[i]))
}
println(buildFooter(total))
}
你现在拥有了一个程序,其页眉和页脚在代码中清晰易读(无需心算 \n),缩进通过 trimMargin() 保持整齐,收据行则通过 padStart()/padEnd() 实现了对齐。 这正是通常推荐的风格:多行字符串加上通过 trimIndent()/trimMargin() 进行的显式缩进控制。
为了巩固印象,这里有一张我们所做操作的小图表(不是代码,而是“思维路线”):
flowchart TD
A["使用 '''''' ...'''''' 编写文本"] --> B["使用 trimIndent/trimMargin 移除格式化缩进"]
B --> C["通过 $var 和 ${expr} 插入数据"]
C --> D["使用 padEnd/padStart 对齐列"]
D --> E["使用 println(...) 输出到控制台"]
4. 使用多行字符串时的常见错误
错误 #1:期望 """...""" 能够自动“完美对齐”。
原始字符串会逐字保留空格和换行符。因此,如果你在代码中带缩进地编写文本,它就会带缩进地打印出来。 解决方法不是“想当然”,而是具体的工具:根据是否需要内部缩进,选择 trimIndent() 或 trimMargin()。
错误 #2:在原始字符串中编写 \n 并期望换行。
在 """...""" 内部,序列 \n 不会转换为换行符;它只是两个普通字符。 换行是通过真实的 Enter 键完成的。如果你想“控制”换行,要么使用真实的换行,要么使用普通字符串 "..."(但今天的重点正是为了不再受 \n 之苦)。
错误 #3:搞混了 trimIndent() 到底移除什么。
trimIndent() 移除所有非空行共有的最小缩进。 如果你想显式控制每一行的边界(例如,部分文本必须“在块内”并有自己的缩进),那么 trimMargin() 通常更清晰:你自己放置 | 并能直观看到有意义的文本从哪里开始。
错误 #4:尝试用空格“肉眼对齐”数字。
当你手动写下 "Tea 3 90" 时,看起来还行,直到出现 "Cappuccino 12 160"。一切立刻就乱套了。 padStart()/padEnd() 解决了这个问题,因为列宽是由数字定义的,而不是由作者的心情决定的。
错误 #5:在不将 Int 转换为 String 的情况下进行对齐。
padStart 和 padEnd 是字符串操作。 如果你尝试直接对数字使用它们,编译器会(义正辞严地)说:“我明白你的意思,但 Int 没有那个方法。” 解决方法很简单:qty.toString().padStart(3),其他数字同理。
错误 #6:在一个巨大的字符串中塞入过多的计算和格式化逻辑。
技术上你可以写一个 200 字符长的怪兽字符串并为它“都在一行里”而感到自豪。 但实际上,两天后你就会问自己:“这是谁写的,他们为什么要害我?” 更成熟的做法是先在 val 中计算好数值(例如 sum),然后再进行字符串格式化。这种方法使代码更具可读性,并降低了格式化错误的风险。
GO TO FULL VERSION