字符串比较的 3 个层次
比较方式 | API | 等价准则 | 复杂度 | 备注 |
---|---|---|---|---|
字符相等 | "==" | 扩展字形簇 canonically equivalent | O(n) | 最常用 |
前缀 | hasPrefix(:) | UTF-8 字节逐段比较 | O(m) | m=前缀长度 |
后缀 | hasSuffix(:) | 同上,从后往前 | O(m) | 注意字形簇边界 |
示例
swift
let precomposed = "café" // U+00E9
let decomposed = "cafe\u{301}" // e + ́
print(precomposed == decomposed) // true ✅ 字形簇等价
let aEnglish = "A" // U+0041
let aRussian = "А" // U+0410 Cyrillic
print(aEnglish == aRussian) // false ❌ 视觉欺骗
Unicode 正规化(Normalization)
有时需要把"视觉上一样"的字符串统一到同一二进制形式,再做哈希或数据库唯一索引。
Swift 借助 Foundation 的 decomposedStringWithCanonicalMapping
/ precomposedStringWithCanonicalMapping
:
swift
import Foundation
func normalized(_ s: String) -> String {
s.decomposedStringWithCanonicalMapping
}
let set: Set<String> = [
normalized("café"),
normalized("cafe\u{301}")
]
print(set.count) // 1 ✅ 去重成功
Swift 5.7+ Regex 一站式入门
字面量构建
swift
import RegexBuilder
let mdLink = Regex {
"[" // 字面左括号
Capture { OneOrMore(.any) } // 链接文字
"]("
Capture { OneOrMore(.any) } // URL
")"
}
let text = "见 [官方文档](https://swift.org)。"
if let match = text.firstMatch(of: mdLink) {
let (whole, title, url) = match.output
print("文字:\(title) 地址:\(url)")
}
性能提示
- 字面量
Regex
在编译期构建,零运行时解析成本; - 捕获组数量 < 5 时,使用静态
Output
类型,无堆分配。
切片 + 区间:一次遍历提取所有信息
需求:把 "/api/v1/users/9527" 拆成版本号与 ID
swift
let path = "/api/v1/users/9527"
// 1. 找到两个数字区间
let versionRange = path.firstRange(of: /v\d+/)! // Swift 5.7 Regex 作为区间
let idRange = path.firstRange(of: /\d+$/)!
// 2. 切片
let version = path[versionRange] // "v1"
let userID = path[idRange] // "9527"
关键点:
path[range]
返回Substring
,长期存需String(...)
。- 正则区间可链式调用,避免多次扫描。
性能 Benchmark(M4 MacBook Pro, Release 构建)
测试 1:100 万次 "==" 比较
swift
import QuartzCore
func measure(action: () -> Void) {
let startTimeinterval = CACurrentMediaTime()
action()
let endTimeinterval = CACurrentMediaTime()
print((endTimeinterval - startTimeinterval) * 1_000)
}
let s1 = "Swift字符串性能测试"
let s2 = "Swift字符串性能测试"
measure { for _ in 0..<1_000_000 { _ = s1 == s2 } }
// 耗时 0.0025 ms
测试 2:100 万次 hasPrefix
swift
measure { for _ in 0..<1_000_000 { _ = s1.hasPrefix("Swift") } }
// 耗时 76 ms
测试 3:提取 Markdown 链接 10 万次
swift
let blog = String(repeating: "见 [官方文档](https://swift.org)。\n", count: 10_000)
measure { _ = blog.matches(of: mdLink).map { $0.output } }
// median 12 ms
结论:
- 比较操作已高度优化,可放心用于字典 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 备份次数。