Swift Regex: Beyond the basics
Swift & UI 进阶 20m

Swift Regex:进阶用法

Swift Regex: Beyond the basics

2022年6月6日

在 Apple 官方观看视频

一句话判断

Swift 5.7 的 Regex 系统远不止是正则表达式的语法糖——它是一套类型安全的模式匹配框架,支持 DSL 构建、强类型捕获、Foundation 解析器集成,以及自定义消费组件。

这场 Session 讲了什么

Swift 5.7 引入了全新的 Regex 类型,但这个 Session 的重点不是基础正则语法,而是那些让 Swift Regex 独树一帜的高级特性。

RegexBuilder DSL。 你可以用声明式的 DSL 语法来构建正则表达式,而不是写一串难以阅读的字符串。每个匹配步骤都是类型安全的,编译器会帮你检查捕获组的类型。

强类型捕获(Strongly-typed captures)。 传统的正则表达式捕获组返回的都是字符串,你需要手动转换类型。Swift Regex 的 TryCaptureCapture 可以直接指定捕获的输出类型,如果转换失败,整个匹配自动失败。

Foundation 解析器集成。 Float.parseFloatStrategyInt.parseIntStrategy.date(format:) 等 Foundation 提供的解析策略可以直接嵌入 Regex,让你在匹配的同时完成数据转换。

CustomConsumingRegexComponent。 你可以创建自己的正则组件,只要实现 consuming 方法即可。这意味着你可以把任何自定义的解析逻辑封装成 Regex 组件,和标准组件无缝组合。

重复行为的精细控制。 默认的正则量词是贪婪的(eager),但 Swift Regex 提供了 .eager.reluctant(又名 lazy)和 .possessive 三种重复行为,让你精确控制匹配策略。

值得深挖的点

类型安全的捕获改变了正则的使用方式。 在其他语言中,正则匹配返回的是 Match 对象,你通过索引取捕获组,然后手动做类型转换。Swift Regex 的 Regex<(Substring, Int, Double)> 这种泛型设计让捕获组的类型在编译期就确定了。这意味着你永远不会因为下标越界或类型转换失败而在运行时崩溃——这类错误在编译期就被消灭了。

RegexBuilder DSL 的可读性是一种权衡。 DSL 语法虽然比原始正则字符串更易读,但它也更冗长。Session 的建议是:简单模式用字符串字面量,复杂模式(特别是需要强类型捕获的)用 DSL。两种方式可以自由混搭,用 Regex 初始化一个子表达式,然后在 DSL 中引用它。

代码片段

import RegexBuilder

// 强类型捕获 + Foundation 解析器
let priceRegex = Regex {
    "$"
    TryCapture {
        OneOrMore(.digit)
        Optionally {
            "."
            Repeat(.digit, count: 2)
        }
    } transform: { raw in
        Float.parseFloat(raw)
    }
    One(.whitespace)
    Capture {
        OneOrMore(.word)
    } transform: { raw in
        raw.lowercased()
    }
}

// 匹配 "$12.99 Coffee" -> (Substring, Float, Substring)
if let match = "$12.99 Coffee".firstMatch(of: priceRegex) {
    let (_, price, item) = match.output
    print("Item: \(item), Price: \(price)")  // 类型安全!
}

// 自定义正则组件
struct IPv4Address: CustomConsumingRegexComponent {
    typealias Output = Substring

    func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) -> (upperBound: String.Index, output: Substring)? {
        let octet = Repeat(.digit, count: 1...3)
        let regex = Regex {
            octet; "."; octet; "."; octet; "."; octet
        }
        return input[index...].firstMatch(of: regex).map { match in
            (match.range.upperBound, match.output)
        }
    }
}

// 重复行为控制
let reluctantMatch = "aaa".firstMatch(of: Regex {
    OneOrMore(.any, .reluctant)  // 尽可能少匹配
    "a"
})
// 匹配 "aa" 而不是 "aaa"

最佳实践

  • 简单的模式用字符串字面量 #"..."#,复杂的或需要类型安全的用 RegexBuilder DSL。
  • 优先使用 TryCapture 而不是 Capture + 手动转换,让编译器帮你处理错误路径。
  • 利用 Foundation 的解析策略(parseIntStrategyparseFloatStrategydate(format:))避免重复造轮子。
  • 对于性能敏感的场景(比如解析大文件),用 .possessive 重复行为来避免不必要的回溯。

还有什么值得关注

  • Regex 类型遵循 Codable,你可以把正则表达式序列化存储。
  • Swift Regex 的底层使用的是基于 NFA 的高效匹配引擎,性能和手写的解析器相当。
  • CharacterClass 提供了大量预定义的字符集(.digit, .word, .whitespace, .hexDigit 等),避免你自己写字符范围。
  • 正则表达式的字面量语法和 DSL 语法可以在同一个表达式中混用。
WWDC 2022