从其他语言转到 Swift 的开发者最容易困惑的地方之一就是字符串。text[0]不行,text[2..<5]也不行,还要创建单独的索引类型并写成text.index(text.startIndex, offsetBy: 2)。在 Python 里,这本来一个字符就能完成。
这不是因为 Swift 团队做不出这样的 API,恰恰相反。它诚实地揭示了“字符串的第 n 个字符”本身并不简单。其他语言选择隐藏复杂性,有时还会给出错误答案;Swift 则选择即使不方便,也始终给出正确答案。这是 Swift 基础系列第 6 篇,也是最后一篇,我们来深入探究字符串真正困难的原因。
将字符串转换为数字时可能失败的流程,会在Swift 中 throws、try 与 Result 的选择指南中结合错误模型进行讲解。
“有几个字符?”为什么很难 — Unicode 的世界
从这个问题开始:“café”有几个字符?看起来当然是 4 个,但对计算机来说有两种答案。é 可以存储为一个预组装码点(U+00E9),也可以由 e(U+0065)和组合重音符号(U+0301)两个码点叠加而成。显示效果相同,但内部码点数量分别是 1 和 2。
韩文让这个问题更加直观。一个韩文音节可以是一个预组装音节码点(U+AC01),也可以是 3 个字母的组合。如果你处理过 macOS 文件名,可能见过以 NFD(Normalization Form D — 将文字拆分为字母单位保存的 Unicode 规范化形式)保存的韩文文件名在其他系统上显示为拆开的字母。同一个“字符”可能包含 1 个或 3 个码点。
表情符号让情况更复杂。家庭表情符号 👨👩👧👦 用零宽连接符(ZWJ,Zero Width Joiner — 将表情符号组合成一个字符的不可见字符)连接 4 个人物表情符号,共有 7 个码点。按 UTF-16 代码单元计算则是 11 个,但在人眼看来仍是一个字符。
因此 Unicode 标准单独定义了“人所感知的一个字符”这一单位,称为扩展字素簇(extended grapheme cluster)。é、组合形式的韩文音节和家庭表情符号分别都是一个字素簇。
语言的选择 — 错误答案更快,还是正确答案更慢?
现在可以比较各语言如何处理“第 n 个字符”。
JavaScript 的"👨👩👧👦".length是 11,因为它计算 UTF-16 代码单元数。Java 也一样。Python 3 的 len 返回 7,因为它计算码点数。这些结果都违背了人类对“一个字符”的直觉。天真地截取包含表情符号的字符串,可能从表情符号中间切开,生成损坏的文字。
Swift 的"👨👩👧👦".count是 1,因为 Swift 的 Character 类型表示字素簇,而不是码点。无论 é 以哪种形式存储,count 都相同;== 也会考虑规范化并返回 true。它始终按照“人眼看到的字符”给出正确答案。
但这种准确性有代价。字素簇长度可变,因此查找第 n 个字符时,必须从开头逐一判断边界并计数。这就是 Swift 字符串没有整数索引的原因。如果提供让text[7]看起来是 O(1) 的 API,实际上就隐藏了 O(n) 的成本。在循环中使用text[i]会变成 O(n²),Swift 选择不鼓励这种写法。不透明的 String.Index 类型通过类型表达了“这个位置是计数后找到的结果”。哲学系列第 1 篇提到的“不隐藏成本”原则,在字符串上同样适用。
实用工具箱 — 无需索引处理字符串
理解原理后进入实践。即使没有整数索引,大多数操作也能用更安全的工具解决。
前后截取使用 prefix 和 suffix。 text.prefix(10)会安全返回前 10 个字符(按字素计算),超出范围也不会崩溃,只返回实际存在的部分。制作预览文本时无需计算 substring 索引。
**搜索使用 range(of:) 和 firstIndex(of:)。**需要位置时,通常是“特定内容的位置”。将返回的 Range 或 Index 直接放入下标即可,无需转换为整数。
分割使用 split。 text.split(separator: ",")取代手动遍历索引。
**如果确实需要第 n 个字符,就使用 Array。**如果字符级算法确实需要随机访问,标准做法是转换为Array(text)。一次支付 O(n) 的转换成本后,之后每次访问都是 O(1),远好于在每次循环中调用 index(offsetBy:)。
**需要字节时,明确指定视图。**网络传输量或 DB 列限制等场景需要的是“字节数”而不是“字符数”。Swift 通过text.utf8.count、text.utf16.count和text.unicodeScalars.count让代码明确表示计数单位。顺便注意,UserDefaults 或旧 API 中的 NSString.length 按 UTF-16 计算,可能不同于 Swift 的 count。
还有一个陷阱:不同字符串的索引互不兼容。把从 a 获取的 String.Index 用在 b 上,可能崩溃或产生错误结果。索引只属于“那个字符串在那个时刻”。修改字符串后,最好将之前的索引视为无效。
这为什么对韩文开发者尤其重要
构建韩文服务时,这些知识会从抽象概念变成实际问题。
昵称长度限制就是典型例子。验证“不超过 10 个字符”时,如果服务器(其他语言)和客户端(Swift)使用不同单位计数,就会出现客户端通过、服务器拒绝的错误。如果服务器按 UTF-8 字节限制,韩文每个字符占 3 个字节,因此 10 个字符就是 30 个字节。团队应先约定“计数什么”;Swift 中再有意识地选择 count(字素)或 utf8.count(字节)。
字母拆分也是一个问题。由于前面提到的 NFD,外部系统传来的韩文字符串可能看起来相同,但码点不同。Swift 的 == 会考虑规范化,因此通常可以直接工作;但把外部字符串作为基于哈希的字典键,或传给其他语言时,使用 precomposedStringWithCanonicalMapping 显式规范化为 NFC(Normalization Form C — 将字母组合为预组装文字保存的规范化形式)会更安全。
在搜索和自动补全中实现首音搜索等功能时,反而需要将字素拆分为字母单位,此时 unicodeScalars 视图是起点。理解视图概念后,这类需求就可以归结为“遍历另一个视图”,而不是“需要特殊库”。
直接运行确认的结果
在 Apple Swift 6.3.3 中,我们比较了预组装形式한和由 3 个字母组成的组合形式한。组合字符串有 3 个 Unicode scalar,但String.count将其计为一个字符,两种表示在规范等价性比较中相等。
string=characters:1,scalars:3,canonically-equal:true
确认结果后,实现长度限制时,我会在变量名中标出单位。对于characterLimit使用count,传输大小使用utf8.count,服务器契约中也写明相同单位。如果只用省略单位的length达成共识,韩文和表情符号很容易导致客户端与服务器不一致。
总结
- “第 n 个字符”之所以困难,是因为 Unicode 中的一个字符(字素簇)由数量可变的码点组成。é、组合形式的韩文和 ZWJ 表情符号都是典型案例。
- 其他语言的 length 可能计算代码单元或码点,从而违背直觉;Swift 的 count 计算字素,始终给出符合人类认知的答案。
- 代价是随机访问变为 O(n),因此 Swift 移除了整数索引,以免隐藏这项成本。
- 实践中,prefix/suffix、range(of:) 和 split 能解决大多数问题;随机访问使用 Array 转换,字节计算则明确使用 utf8 视图。
- 韩文服务的实践重点是约定字符数验证单位,并处理 NFC/NFD 规范化。
基础系列第 6 篇到此结束。接下来是中级系列。第一个主题是 Swift 内存管理的核心:ARC(Automatic Reference Counting,自动引用计数)以及 weak 与 unowned 的选择。这是闭包篇预告的循环引用故事的正式篇章。
延伸阅读
- [Swift 基础 #5] Swift 错误处理完整地图:throws、try?、try!、Result 什么时候用哪个
- [Swift 基础 #4] 正确使用 Swift guard:用提前退出压平毁灭金字塔
- iOS Coordinator 模式:将页面切换代码从视图控制器中移出
来源与验证
- Swift StringApple Developer Documentation · 官方文档 · 核查 2026年8月26日依据: String 的 Character 集合模型、Unicode 规范等价性、字符串视图和索引 API
- The Swift Programming Language: Strings and CharactersSwift.org · 官方文档 · 核查 2026年8月26日依据: 扩展字素簇、韩文组合表示,以及 count 和整数索引的成本说明

![[Swift 基础 #6] 为什么字符串不能使用 text[0]? 封面图](/assets/images/posts/18529e8e-628f-4b41-8e48-680d2ff6f480/1.jpg)