Swift 字符串与字符完全导读(三):比较、正则、性能与跨平台实战

字符串比较的 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 调试

终极最佳实践清单

  1. 比较:优先用 "==",必要时先正规化再哈希。
  2. 前缀/后缀:用 hasPrefix / hasSuffix,别手写 prefix() 再比较。
  3. 索引:永远通过 String.Index 计算,禁止 str[Int]
  4. 子串:函数返回前立即 String(substring),防止隐式内存泄漏。
  5. 拼接:大量小字符串先用 [String] 收集,最后 joined();或 reserveCapacity 预分配。
  6. 正则:静态字面量 Regex 性能最佳;捕获组能少就少。
  7. 遍历:
    • 看"人眼字符"→ for ch in string
    • 看"UTF-8 字节"→ string.utf8
    • 看"Unicode 标量"→ string.unicodeScalars
  8. 多线程:String 是值类型,跨线程传递无数据竞争,但共享大字符串时 Substring 会拖住原内存,及时转存。
  9. 日志 / 模板:多行字面量 + 插值最清晰;需要原始反斜杠用扩展分隔符 #"..."#
  10. 性能测量:用 swift test -c release + measure 块, Instruments 只看 "Swift String" 的 CoW 备份次数。
相关推荐
HarderCoder4 小时前
Swift 字符串与字符完全导读(一):从字面量到 Unicode 的实战之旅
swift
HarderCoder4 小时前
Swift 字符串与字符完全导读(二):Unicode 视图、索引系统与内存陷阱
swift
非专业程序员Ping12 小时前
一文读懂字体文件
ios·swift·assembly·font
wahkim16 小时前
移动端开发工具集锦
flutter·ios·android studio·swift
非专业程序员Ping1 天前
一文读懂字符、字形、字体
ios·swift·font
东坡肘子1 天前
去 Apple Store 修手机 | 肘子的 Swift 周报 #0107
swiftui·swift·apple
非专业程序员2 天前
iOS/Swift:深入理解iOS CoreText API
ios·swift
xingxing_F2 天前
Swift Publisher for Mac 版面设计和编辑工具
开发语言·macos·swift
YGGP3 天前
【Swift】LeetCode 438. 找到字符串中所有字母异位词
swift