字符串比较的 3 个层次
| 比较方式 | API | 等价准则 | 复杂度 | 备注 |
|---|---|---|---|---|
| 字符相等 | “==” | 扩展字形簇 canonically equivalent | O(n) | 最常用 |
| 前缀 | hasPrefix(:) | UTF-8 字节逐段比较 | O(m) | m=前缀长度 |
| 后缀 | hasSuffix(:) | 同上,从后往前 | O(m) | 注意字形簇边界 |
示例
1let precomposed = "café" // U+00E9 2let decomposed = "cafe\u{301}" // e + ́ 3print(precomposed == decomposed) // true ✅ 字形簇等价 4 5let aEnglish = "A" // U+0041 6let aRussian = "А" // U+0410 Cyrillic 7print(aEnglish == aRussian) // false ❌ 视觉欺骗 8
Unicode 正规化(Normalization)
有时需要把“视觉上一样”的字符串统一到同一二进制形式,再做哈希或数据库唯一索引。
Swift 借助 Foundation 的 decomposedStringWithCanonicalMapping / precomposedStringWithCanonicalMapping:
1import Foundation 2 3func normalized(_ s: String) -> String { 4 s.decomposedStringWithCanonicalMapping 5} 6 7let set: Set<String> = [ 8 normalized("café"), 9 normalized("cafe\u{301}") 10] 11print(set.count) // 1 ✅ 去重成功 12
Swift 5.7+ Regex 一站式入门
字面量构建
1import RegexBuilder 2 3let mdLink = Regex { 4 "[" // 字面左括号 5 Capture { OneOrMore(.any) } // 链接文字 6 "](" 7 Capture { OneOrMore(.any) } // URL 8 ")" 9} 10 11let text = "见 [官方文档](https://swift.org)。" 12if let match = text.firstMatch(of: mdLink) { 13 let (whole, title, url) = match.output 14 print("文字:\(title) 地址:\(url)") 15} 16
性能提示
- 字面量
Regex在编译期构建,零运行时解析成本; - 捕获组数量 < 5 时,使用静态
Output类型,无堆分配。
切片 + 区间:一次遍历提取所有信息
需求:把 “/api/v1/users/9527” 拆成版本号与 ID
1let path = "/api/v1/users/9527" 2// 1. 找到两个数字区间 3let versionRange = path.firstRange(of: /v\d+/)! // Swift 5.7 Regex 作为区间 4let idRange = path.firstRange(of: /\d+$/)! 5 6// 2. 切片 7let version = path[versionRange] // "v1" 8let userID = path[idRange] // "9527" 9
关键点:
path[range]返回Substring,长期存需String(...)。- 正则区间可链式调用,避免多次扫描。
性能 Benchmark(M4 MacBook Pro, Release 构建)
测试 1:100 万次 “==” 比较
1import QuartzCore 2func measure(action: () -> Void) { 3 let startTimeinterval = CACurrentMediaTime() 4 action() 5 let endTimeinterval = CACurrentMediaTime() 6 print((endTimeinterval - startTimeinterval) * 1_000) 7} 8 9let s1 = "Swift字符串性能测试" 10let s2 = "Swift字符串性能测试" 11 12measure { for _ in 0..<1_000_000 { _ = s1 == s2 } } 13// 耗时 0.0025 ms 14
测试 2:100 万次 hasPrefix
1measure { for _ in 0..<1_000_000 { _ = s1.hasPrefix("Swift") } } 2// 耗时 76 ms 3
测试 3:提取 Markdown 链接 10 万次
1let blog = String(repeating: "见 [官方文档](https://swift.org)。\n", count: 10_000) 2measure { _ = blog.matches(of: mdLink).map { $0.output } } 3// median 12 ms 4
结论:
- 比较操作已高度优化,可放心用于字典 Key;
- 正则采用静态构建后,与手写 Scanner 差距 < 5%。
常见“坑”与诊断工具
| 场景 | 现象 | 工具/修复 |
|---|---|---|
| Substring 泄漏 | 百万行日志内存暴涨 | Instruments → Allocations → 查看 “Swift String” 的 CoW 备份 |
| 整数下标越界 | 运行时 crash | 使用 index(_, offsetBy:, limitedBy:) 安全版 |
| 正则回溯爆炸 | 卡住 100% CPU | 在 Regex 内使用 Possessive 量词或 OneOrMore(..., .eager) |
| 比较失败 | “é” != “é” | 检查是否混入 Cyrillic / Greek 等视觉同形字符;打印 unicodeScalars 调试 |
终极最佳实践清单
- 比较:优先用 “==”,必要时先正规化再哈希。
- 前缀/后缀:用
hasPrefix/hasSuffix,别手写prefix()再比较。 - 索引:永远通过
String.Index计算,禁止str[Int]。 - 子串:函数返回前立即
String(substring),防止隐式内存泄漏。 - 拼接:大量小字符串先用
[String]收集,最后joined();或reserveCapacity预分配。 - 正则:静态字面量
Regex性能最佳;捕获组能少就少。 - 遍历:
- 看“人眼字符”→
for ch in string - 看“UTF-8 字节”→
string.utf8 - 看“Unicode 标量”→
string.unicodeScalars
- 看“人眼字符”→
- 多线程:String 是值类型,跨线程传递无数据竞争,但共享大字符串时 Substring 会拖住原内存,及时转存。
- 日志 / 模板:多行字面量 + 插值最清晰;需要原始反斜杠用扩展分隔符
#"..."#。 - 性能测量:用
swift test -c release+measure块, Instruments 只看 “Swift String” 的 CoW 备份次数。
《Swift 字符串与字符完全导读(三):比较、正则、性能与跨平台实战》 是转载文章,点击查看原文。
