Unicode 的三种编码视图
Swift 把同一个 String 暴露成 4 种迭代方式:
| 视图 | 元素类型 | 单位长度 | 典型用途 |
|---|---|---|---|
| String | Character | 人眼“一个字符” | 业务逻辑 |
| utf8 | UInt8 | 1~4 字节 | 网络/文件 UTF-8 流 |
| utf16 | UInt16 | 2 或 4 字节 | 与 Foundation / Objective-C 交互 |
| unicodeScalars | UnicodeScalar | 21-bit | 精确到标量,做编码分析 |
代码一览
1let dog = "Dog‼🐶" 2// 4 个 Character,5 个标量,10 个 UTF-8 字节,6 个 UTF-16 码元 3 4// 1. Character 视图 5for ch in dog { 6 print(ch, terminator: "|") 7} // D|o|g|‼|🐶| 8print() 9 10// 2. UTF-8 11dog.utf8.forEach { 12 print($0, terminator: " ") 13}// 68 111 103 226 128 188 240 159 144 182 14print() 15 16// 3. UTF-16 17dog.utf16.forEach { 18 print($0, terminator: " ") 19}// 68 111 103 8252 55357 56374 20print() 21 22// 4. Unicode Scalars 23dog.unicodeScalars.forEach { 24 print($0.value, terminator: " ") 25}// 68 111 103 8252 128054 26print() 27
扩展字形簇 vs 字符计数
1var cafe = "cafe" 2print(cafe.count) // 4 3cafe += "\u{301}" // 附加组合重音 4print(cafe, cafe.count) // café 4 (仍然是 4 个 Character) 5
结论:
count走的是“字形簇”边界,必须从头扫描,复杂度 O(n)。- 不要在大循环里频繁读取
str.count;缓存到局部变量。
String.Index 体系
基础 API
1let str = "Swift🚀" 2let start = str.startIndex 3let end = str.endIndex // 指向最后一个字符之后 4// let bad = str[7] // ❌ Compile-time error:Index 不是 Int 5 6let fifth = str.index(start, offsetBy: 5) 7print(str[fifth]) // 🚀 8
往前/后偏移
1let prev = str.index(before: fifth) 2let next = str.index(after: start) 3let far = str.index(start, offsetBy: 4, limitedBy: end) // 安全版,返回可选值 4
区间与切片
1let range = start...fifth 2let sub = str[range] // Substring 3
子串 Substring 的“零拷贝”双刃剑
1let article = "Swift String 深度指南" 2let intro = article.prefix(9) // Substring 3// 此时整份 article 的缓冲区仍被 intro 强引用,内存不会释放 4 5// 正确姿势:尽快转成 String 6let introString = String(intro) 7
内存图简述
1article ┌-------------------------┐ 2 │ Swift String 深度指南 │ 3 └-------------------------┘ 4 ▲ 5 │零拷贝 6 intro (Substring) 7
只要 Substring 活着,原 String 的缓冲区就不得释放。
最佳实践:函数返回时立刻 String(substring),避免“隐形内存泄漏”。
插入、删除、Range 替换全 API 速查
1var s = "Hello Swift" 2 3// 插入字符 4s.insert("!", at: s.endIndex) 5// Hello Swift! 6print(s) 7 8// 插入字符串 9s.insert(contentsOf: " 2025", at: s.index(before: s.endIndex)) 10// Hello Swift 2025! 11print(s) 12 13// 删除单个字符 14s.remove(at: s.firstIndex(of: " ")!) // 删掉第一个空格 15// HelloSwift 2025! 16print(s) 17 18// 删除子范围 19let range = s.range(of: "Swift")! 20s.removeSubrange(range) 21// Hello 2025! 22print(s) 23 24// 直接替换 25s.replaceSubrange(s.range(of: "2025")!, with: "2026") 26// Hello 2026! 27print(s) 28
实战:写一个“安全截断”函数
需求
- 按“字符数”截断,但不能把 Emoji/组合音标劈成两半;
- 尾部加“...”且总长度不超过 maxCount;
- 返回 String,而非 Substring。
代码
1func safeTruncate(_ text: String, maxCount: Int, suffix: String = "...") -> String { 2 guard maxCount > suffix.count else { return suffix } 3 let maxTextCount = maxCount - suffix.count 4 var count = 0 5 var idx = text.startIndex 6 while idx < text.endIndex && count < maxTextCount { 7 idx = text.index(after: idx) 8 count += 1 9 } 10 // 如果原文很短,无需截断 11 if idx == text.endIndex { return text } 12 return String(text[..<idx]) + suffix 13} 14 15// 测试 16let long = "Swift 字符串深度指南🚀🚀🚀" 17print(safeTruncate(long, maxCount: 12)) // "Swift 字符..." 18
复杂度 O(n),只扫描一次;不依赖 count 的重复计算。
性能与内存最佳实践清单
- 大量拼接用
String.reserveCapacity(_:)预分配。 - 遍历+修改时先复制到
var,再批量改,减少中间临时对象。 - 网络/文件 IO 用
utf8视图直接写入Data,避免先转String。 - 正则提取到的
[Substring]尽快 map 成[String]再长期持有。 - 不要缓存
str.count在多次循环外,如果字符串本身在变。
扩展场景:今天就能落地的 3 段代码
日志脱敏(掩码手机号)
1func maskMobile(_ s: String) -> String { 2 guard s.count == 11 else { return s } 3 let start = s.index(s.startIndex, offsetBy: 3) 4 let end = s.index(s.startIndex, offsetBy: 7) 5 return s.replacingCharacters(in: start..<end, with: "****") 6} 7
语法高亮(简易关键词着色)
1let keywords = ["let", "var", "func"] 2var code = "let foo = 1" 3for kw in keywords { 4 if let range = code.range(of: kw) { 5 code.replaceSubrange(range, with: "[KW]\(kw)[KW]") 6 } 7} 8
大文件分块读(UTF-8 视图直接操作)
1import Foundation 2func chunk(path: String, chunkSize: Int = 1<<14) -> [String] { 3 guard let data = FileManager.default.contents(atPath: path) else { return [] } 4 return data.split(separator: UInt8(ascii: "\n"), 5 maxSplits: .max, 6 omittingEmptySubsequences: false) 7 .map { String(decoding: $0, as: UTF8.self) } 8} 9
利用 UInt8 切片,避免先整体转成 String 的额外内存峰值。

