第073篇 字符串模板与原生字符串:多行文本的正确姿势

简介: Kotlin字符串模板核心三点:①编译后转为`StringBuilder.append`链(全常量时优化为字面量);②原生字符串`"""..."""`保留缩进,必须用`trimIndent()`或`trimMargin()`清理;③Android中禁在循环内用模板拼接,否则触发O(n²)性能坑。安全上,模板不用于SQL/URL等需解析的场景。

字符串模板这题的"结论"很简单:"$name"、"${expr}"、"""..."""。但真正值得讲的有三件事——模板在编译后变成了什么(StringBuilder 还是常量折叠)、原生字符串的 trimIndent 为什么必需、以及在 Android 上一个高频性能坑:循环里用模板做字符串拼接。这三点能从"语法"讲到"字节码"再到"性能",层次就出来了。

先把结论放在前面:Kotlin 的字符串模板 "Hello, $name" 编译后走 StringBuilder.append 链(等价于 Java 的 "Hello, " + name);而所有插值部分都是常量时,编译器会做常量折叠,直接生成一个字符串常量。原生字符串 """...""" 支持多行、不转义引号与反斜杠,但会保留源码的缩进,所以几乎总是要配 trimIndent()。$ 本身要用 \$ 转义,${'$'} 可以输出美元符号。

机制拆解

先看模板的编译。"a${b}c$d" 编译成 new StringBuilder().append("a").append(b).append("c").append(d).toString()。如果所有片段都是编译期常量("a" + 1 + "c" 里的 1 是字面量),编译器会折叠成一个常量字符串。这个优化的存在意味着:写 "${list.size} 个" 在 list 为局部变量时不会有额外开销,但写 "$a$b" 在两个都是 var 引用时就会构造 StringBuilder。 差别不大,但解释了为什么常量拼接的日志不会拖慢性能。

原生字符串的实现更值得说。"""...""" 在编译期会把源文件的原始文本(包括换行与缩进)作为常量取出,所以缩进被完整保留。trimIndent() 的作用是:检测所有行的最小公共缩进并移除它,同时去掉首尾空行。trimMargin() 则是另一种常见用法——用 | 作前缀标记,只去掉带 | 的行的前缀,保留代码块的相对缩进(比如嵌在 Markdown 代码块里)。Kotlin 官方文档里的 KDoc 示例就是用 | 的。

$ 的转义规则也常被问:模板里 $ 后面跟标识符或 { 才是插值,所以纯美元符号写成 \$;如果 \$ 在原生字符串里就不生效(原生字符串不处理转义),需要用 ${'$'} 这种写法。中文场景里还有一类实际问题:金额与用户输入的拼接——"$name 余额 $money 元" 在用户名字里含特殊字符时没问题,但如果模板内容被用来做 URL 或 SQL,就会带来注入风险。

这些坑的正确绕法

最常见的坑是原生字符串忘记 trimIndent,输出带着层级缩进直接进了展示层。 表现是日志里多出一堆空格,UI 上多出一段缩进;更糟的情况是把带缩进的字符串拿去做等值比较或缓存 key,导致始终匹配不上。根因是原生字符串忠实保留了源码缩进,而人眼在源码里看不出来。修法是所有多行原生字符串一律加 .trimIndent(),或者在需要精确控制时用 trimMargin("|")。

其次是循环里用字符串模板做累积,产出大量临时对象。 表现是处理大列表时 GC 频繁、卡顿;单看代码很简洁,完全看不出问题。根因是每次迭代都新建 StringBuilder 并 toString() 生成新字符串,复杂度从线性退化成平方级。修法是显式声明一个 StringBuilder 在循环外累积,最后 toString() 一次;或者收集到列表后用 joinToString。这一条是 Android 侧最实际的性能点。

还有一个更隐蔽的坑:模板里嵌 null 打出字面量 "null"。 表现是用户 id 为空时,日志和界面显示成"用户 null",排查时误以为传入了字符串 "null"。Kotlin 里 "$maybeNull" 在 maybeNull 为 null 时输出 "null"(不抛异常)。修法是显式处理:maybeNull?.toString() ?: "未知",或在展示层用 Elvis。

代码里见真章

看一段能直接跑的代码,把上面的机制落到具体写法上:

val name = "amy"
val age = 30
val msg1 = "用户 $name,$age 岁"                 // 编译成 StringBuilder 链
val msg2 = "用户 ${if (age > 18) "成年" else "未成年"}"   // 表达式可嵌
val msg3 = "价格 $1.5"                          // 输出 1.5,但建议写 ${
   '$'}1.5
val dollar = "100\$"                            // 纯美元符需转义
val dollar2 = "100${'$'}"                       // 或用表达式写法

// 原生字符串:多行 + 不转义,但保留缩进
val raw = """
    line1
    line2
"""
val fixed = raw.trimIndent()                     // 缩进被清理,必须写
val md = """
    |# 标题
    |正文
    |- 项目
""".trimMargin("|")                              // 按 | 标记去前缀,保留相对缩进

// 1) 循环里用模板 = 平方级开销(反例)
var s1 = ""
repeat(1000) {
    i -> s1 += "item$i " }            // 每次新建 StringBuilder

// 2) 正解:显式 StringBuilder 提到循环外
val sb = StringBuilder(1024)
for (i in 0 until 1000) sb.append("item").append(i).append(' ')
val s2 = sb.toString()

// 3) 或者收集后一次性 join
val s3 = (0 until 1000).joinToString(" ") {
    "item$it" }

// 4) null 会打成字面量 "null"
val maybe: String? = null
val bad = "用户 $maybe"                          // "用户 null"
val good = "用户 ${maybe ?: "未知"}"               // "用户 未知"

// 5) 模板不要用来拼 SQL 或 URL(注入与转义风险)
// 错误:val sql = "SELECT * FROM t WHERE name = '$name'"
val safe = "q=$name".let {
    java.net.URLEncoder.encode(it, "UTF-8") }  // 编码后再拼

这段代码值得盯三处:第一处,fixed 与 md 两个 trimIndent/trimMargin 用法并列,说明两种清理缩进的方式;第二处,第 1~3 段把"循环里用模板"的反例与两种正解放在一起,这是 Android 侧最容易踩的性能点;第三处,maybe ?: "未知" 是处理 null 拼接的标准写法。

这题在面试里怎么问、怎么答

"字符串模板编译后是什么?"答:变量插值走 StringBuilder.append 链(等价于 Java 的 + 拼接);所有片段都是编译期常量时会被常量折叠成一个字符串常量。所以 "a" + 1 + "b" 这种全常量的拼接不产生 StringBuilder,而 "a" + name + "b" 会。

"trimIndent 的算法是什么?"答:算出所有非空行的最小缩进量,统一移除;再去掉首尾的空行。要注意它对"第一行紧跟 """ 的情况"处理方式与后续行不同,这是缩进不统一时 trimIndent 结果异常的常见原因——解决办法是让 """ 后面直接换行,所有内容从下一行开始写。

"和 Java 的字符串拼接有什么区别?"答:语法上都是拼接,但三点不同:①Kotlin 模板里可以直接写任意表达式,Java 需要字符串连接或 String.format;②Kotlin 的 trimIndent 是语言级能力,Java 要手写 stripIndent(Java 13 才有);③Kotlin 模板对 null 渲染成 "null" 而 Java 的 + 同样如此,两边一致,不是差异点。

"使用中遇到过什么问题?"案例一:多行文案用了原生字符串忘记 trimIndent,导致 UI 顶部多出缩进;修复为统一加 trimIndent。案例二:循环里用模板拼一个大字符串,列表 500 项时卡顿明显;改用循环外 StringBuilder 后卡顿消失。

再补一个工程上值得讲清的一点:Android 上要警惕"隐式字符串模板带来的性能与安全双风险"。 性能侧上面已讲(循环拼接);安全侧是不要用模板拼任何进入解释器的字符串——SQL、Shell 命令、URL 路径、WebView 的 JavaScript bridge 参数,都应该用参数化查询或编码后的值。模板让拼字符串变得太顺手,反而容易埋雷。养成习惯:模板只用于"给人看的文本",需要被解析的内容一律走参数化。这一条能在面试里体现安全意识,比讲语法有用得多。

给正在准备面试的你

把这题画成一张"三种字符串形态"的对照图,三列分别是普通引号 ""、原生 """"""、带 trimIndent/trimMargin 的原生串。每列标注四行特性:是否转义、是否多行、是否保留缩进、Java 侧调用形态(String/StringBuilder)。图下方画一条红色的性能提示,画一个"循环 1000 次用模板"的方框打个叉,旁边写"O(n²)",再画一个"循环外 StringBuilder"的方框打勾。面试时从这张图讲到性能与安全,层次就出来了。

再补工程案例与踩坑——应用落点是给项目里所有多行字符串加 trimIndent/trimMargin,把循环内的模板拼接改成循环外 StringBuilder 或 joinToString,为"用户输入拼进 URL/SQL"的地方补编码或参数化,并对展示层做一次 null 渲染的排查。

复习时别孤立刷题:中缀表达式与运算符重载——repeat(n) { } 就是 Kotlin 自带的中缀用法,"a" times 3 则是自定义的。

划两句重点:模板编译成 StringBuilder 链,全常量时折叠;原生字符串保留缩进,必须 trimIndent/trimMargin;$ 要转义;循环里用模板是平方级开销;模板不用于拼 SQL 与 URL;null 会渲染成字面量 "null"。

下一篇聊区间与遍历:until、downTo 与 step——沿着今天这条主线继续往前走。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:中缀表达式与运算符重载:可读性的双刃剑

下一篇预告:区间与遍历:until、downTo-与-step

有任何问题欢迎在评论区留言交流。

相关文章
|
25天前
|
自然语言处理 Java Android开发
第072篇 中缀表达式与运算符重载:可读性的双刃剑
Kotlin运算符重载比Java更彻底:不仅支持`+ - * /`等映射为`plus`/`minus`等约定函数,还允许任意单参函数通过`infix`声明实现中缀调用(如`a to b`)。核心原则是——符号语义必须与原始含义一致(`+`即相加,`-`即取反或相减),滥用将损害可读性。
78 1
|
17天前
|
Ubuntu Linux iOS开发
【2026最新版】VirtualBox下载安装保姆级教程(从零搭建VirtualBox虚拟机)
VirtualBox 是免费开源的跨平台虚拟机软件,支持 Windows/macOS/Linux 宿主机,可同时运行多个操作系统(如 Windows、Linux),用于测试、开发与学习。具备文件共享、剪贴板互通、USB 设备支持等功能,需安装扩展包启用高级特性。图形性能弱于 VMware,但轻量易用。(239 字)
|
7天前
|
人工智能 IDE 开发工具
建议 Qoder 原生支持 Agent 轨迹复盘、过程评测与改进闭环
Qoder、AI Coding、Agent、轨迹复盘、过程评测、研发效能
130 3
|
14天前
|
Java Linux 开发工具
【保姆级】IDEA官网下载+安装+配置+使用教程(所有版本汇总,2026最新版)
IntelliJ IDEA 是 JetBrains 开发的主流 Java IDE,支持 Java、Kotlin、Spring 等开箱即用,提供智能代码分析、重构与调试。含免费社区版(适合学习/中小型项目)和付费 Ultimate 版(含微服务、数据库、远程开发等高级功能),全面支持 Windows/macOS/Linux。
|
15天前
|
人工智能 编解码 物联网
ComfyUI-v9.5 整合包部署教程:ComfyUI+A1111 双环境搭建,附脚本与批量出图 API 代码
秋叶ComfyUI-v9.5网盘资源包,集成ComfyUI节点版与A1111表单版双环境,模型通用。含SD/MJ提示词库、完整模型插件及视频教程,支持PowerShell一键迁移与Python API批量绘图,适配漫剧/短视频高效创作。(239字)
|
21天前
|
弹性计算 缓存 测试技术
阿里云服务器新老用户活动价格全览:38元起续费同价、99元e到九代c9a全对比‌
本文将对当前阿里云服务器活动价格体系进行系统性梳理,涵盖特惠款机型、新用户专享规格、新老用户同享规格、九代企业级实例及优惠券叠加策略等多个维度,为采购决策提供可量化、可验证、可操作的参考依据。
|
1月前
|
人工智能 API
阿里云Coding Plan还有吗?在申请订阅?百炼看到Token Plan了,Coding Plan在哪?
阿里云百炼CodingPlan是AI编程专属订阅套餐,仅提供Pro版(200元/月,首月39.9元),含9万次/月调用额度,支持Qwen、Kimi等十余款模型,需专用API Key,限交互式使用。
|
2月前
|
机器学习/深度学习 算法 前端开发
发动机故障诊断智能体(二):基础时序压缩与长短期记忆编码网络
在智能体完成气路多航段时序数据的采集与规整后,高维度的原始时序信号需要经过深度表征与降维处理,以消除高频测量噪声并提炼本质物理趋势。采用长短期记忆自编码器架构,在脱离人工标签监督的情况下,利用海量正常运行数据进行自监督重构训练,构建出能够压缩并表征发动机稳态工况与退化趋势的时序特征提取模型。
|
2月前
|
人工智能 安全 API
阿里云Token Plan 个人版Quick Start:最低39 元/月,主流AI工具直接接入,Credits 统一计量
阿里云百炼 Token Plan 个人版是面向个人开发者的 AI 大模型订阅服务,采用 Credits 统一计量,支持在 Claude Code、Cursor、Qwen Code 等主流 AI 编程和智能体工具中使用。目前提供 Lite、Standard、Pro 三档套餐及用量包,限时价格分别为 39 元/月、139 元/月、499 元/月。
阿里云Token Plan 个人版Quick Start:最低39 元/月,主流AI工具直接接入,Credits 统一计量

热门文章

最新文章