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