ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Swift 正则字面量(SE-0354)完全指南:`/.../` 与 `/.../` 的语法、类型推断与解析规则

Swift 正则字面量(SE-0354)完全指南:`/.../` 与 `/.../` 的语法、类型推断与解析规则 Swift 正则字面量SE-0354完全指南/.../与#/.../#的语法、类型推断与解析规则【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolution本指南以 Swift Evolution 提案 SE-0354 Regex Literals 为核心系统讲解 Swift 5.7 起引入的正则字面量语法如何用/.../与扩展定界符#/.../#在编译期构造Regex如何通过捕获组自动推断强类型输出以及编译器解析/.../时与注释、运算符之间的歧义处理与规避手段。读完本文你将掌握在 Swift 源码中编写编译期校验、强类型捕获的正则的完整实战技能并能根据提案中的解析规则写出与现有运算符、注释语法兼容的代码。背景为什么需要正则字面量在 Swift 的正则体系中SE-0350 Regex Type and Overview 引入了RegexOutput类型它可以在运行时从字符串动态编译正则模式。例如let pattern #(\w)\s\s(\S)\s\s((?:(?!\s\s).)*)\s\s(.*)# let regex try! Regex(pattern) // regex: RegexAnyRegexOutput运行时编译对于用户输入等动态场景是必要的比如 SwiftPM 的swift test --filter但当模式是静态已知时这种方式存在明显缺陷错误延迟到运行时正则语法错误直到运行时才被诊断且必须显式处理错误如try!缺乏工具链支持无法获得语法高亮、代码补全、重构等源码工具能力捕获类型未知捕获类型在运行时才确定只能使用动态的AnyRegexOutput类型擦除输出语法过于冗长作为匹配函数的参数时尤其明显。而 SE-0355 Regex Syntax and Run-time Construction 定义的语法集、以及 SE-0351 Regex builder DSL 的结果构建器 API为正则字面量提供了解析引擎与类型推断基础——字面量的内容会被编译器直接按 SE-0355 规定的正则语法解析任何错误在编译期即被诊断。核心方案两种字面量语法基本形式/.../// Matches identifier hexadecimal value, extracting the identifier and hex number let regex /(?identifier[[:alpha:]]\w*) (?hex[0-9A-F])/ // regex: Regex(Substring, identifier: Substring, hex: Substring)提案指出选择正斜杠作为定界符是正则领域的“术语传统”它可追溯至 1969 年第一个 Unix 编辑器 ed随后被 less、vim、sed、Perl、Ruby、JavaScript 等一脉相承拥有超过五十年的先例。常见的替代方案——把模式塞进普通字符串字面量再传给库 API——通常带来额外开销、更多转义并把语法错误推迟到运行时而 Swift 正则字面量没有这些限制。扩展形式#/.../#与多行字面量Perl 和 Ruby 允许用户选择定界符以避免转义正则内部的斜杠。Swift 同样提供扩展字面量#/.../#可在字面量周围放置任意数量的平衡#字符let regex #/usr/lib/modules/([^/])/vmlinuz/# // regex: Regex(Substring, Substring)当开定界符后紧跟换行时字面量变为多行字面量此时空白无语义、行尾注释被忽略let regex #/ usr/lib/modules/ # Prefix (?subpath [^/]) /vmlinuz # The kernel /# // regex: Regex(Substring, subpath: Substring)多行模式下自动启用 SE-0355 中的扩展正则语法(?x)正则内的空白包括字符类内部变为无语义# comment形式支持行尾注释。与 Regex builder DSL 的混用正则字面量可以作为RegexComponent直接嵌入结果构建器实现简洁匹配语法与显式强类型语法的自由切换// A regex for extracting a currency (dollars or pounds) and amount from input // with precisely the form /[$£]\d\.\d{2}/ let regex Regex { Capture { /[$£]/ } TryCapture { /\d/ . /\d{2}/ } transform: { Amount(twoDecimalPlaces: $0) } }正如 SE-0350 所述“Regex本身是结果构建器的合法组件”字面量与 DSL 的无缝组合正是这一设计意图的直接体现。类型化捕获编译期推断捕获类型正则字面量的捕获类型由捕获组静态确定推断规则如下整个匹配始终产生一个Substring若存在捕获组则形成以Substring开头的元组后续元素依次对应各捕获组类型顺序遵循 SE-0355 规定的捕获组编号捕获默认类型为Substring但当捕获不保证在成功匹配时必有值时会被包装为可选。可选捕获的触发条件以下情况捕获会被包装为可选嵌套在可能匹配零次的量词内?、*、以及下界为 0 的区间量词如{0,n}出现在交替alternation的某个分支中。let regex1 /([ab])?/ // regex1: Regex(Substring, Substring?) let regex2 /([ab])|\d/ // regex2: Regex(Substring, Substring?)嵌套在捕获内部而非捕获本身被零量词/交替包围的量词或交替不会产生可选捕获——此时零次匹配得到的是空字符串let regex /([ab]*)cd/ // regex: Regex(Substring, Substring)可选性不会多层嵌套最多只应用一层。例如let regex /(.)*|\d/ // regex: Regex(Substring, Substring?)提案明确指出这一行为与 DSL 不同DSL 受限于当前结果构建器的能力在类似场景会应用多层可选性。命名捕获推断元组标签命名捕获组是字面量目前独有的类型化捕获特性——编译器会为命名捕获组推断出带标签的元组元素func matchHexAssignment(_ input: String) - (String, Int)? { let regex /(?identifier[[:alpha:]]\w*) (?hex[0-9A-F])/ // regex: Regex(Substring, identifier: Substring, hex: Substring) guard let match input.wholeMatch(of: regex), let hex Int(match.hex, radix: 16) else { return nil } return (String(match.identifier), hex) }这样捕获既可按名称访问match.identifier、match.hex也可像匿名捕获组一样按编号访问match.1、match.2。这一标签推断在 DSL 中不可用但 DSL 用户可以将捕获绑定到命名变量见 SE-0351 的 Capture and Reference 部分。扩展定界符细节反斜杠的转义语义扩展定界符#/.../#与原始字符串字面量#...#SE-0200有一个关键区别反斜杠不成为字面字符仍保留正则语义。字符串#\n#表示字面字符\n而正则#/\n/#仍是换行转义序列。这一设计的动机在于可移植性正则表达式经常从外部文件或工具中原样拷贝而来若反斜杠失去语义则必须为适配不同定界符而调整转义序列。相比之下字符串字面量中反斜杠可能对“消费者”有语义如NSRegularExpression因此默认按字面处理更合适// Matches \ word char whitespace* whitespace* digit let regex try NSRegularExpression(pattern: \\\\\\w\\s*\\s*\\d, options: [])同一正则用字面量可直接书写为let regex /\\\w\s*\s*\d/ // regex: RegexSubstring因为正则解析器是这类转义序列唯一的消费者不存在歧义。反斜杠本身作为字面仍需转义\\但提案认为其出现频率远低于\s、\w、\p{...}等正则转义序列。多行字面量的更多规则let regex #/ # Match a line of the format e.g DEBIT 03/03/2022 Totally Legit Shell Corp $2,000,000.00 (?kind \w) \s\s (?date \S) \s\s (?account (?: (?!\s\s) . )) \s\s # Note that account names may contain spaces. (?amount .*) /#多行模式适用于任意非零数量的#定界符。与多行字符串SE-0168类似闭定界符必须出现在新行上为避免解析混淆若缺少闭定界符整个字面量不会被解析——这样只输入开定界符时不会意外把文件其余部分当作正则。需要注意的限制多行字面量中的扩展语法不能用(?-x)整体关闭但可以在组(?-x:...)或引号序列\Q...\E内关闭只要它们不跨多行如需换行可写\n或用反斜杠转义字面换行符let regex #/ a\ b\ c /# // regex /a\nb\nc/这与多行字符串的语义形成鲜明对比多行字符串中\换行会去除缩进与换行 a b c而多行正则字面量保留为真正的换行/a\nb\nc/。与注释语法的歧义行注释//与块注释/*会继续按注释解析空正则字面量几乎无实用价值如需表达可写#//#*不是正则的合法起始字符因此不会构成问题。真正需要警惕的是块注释包裹以*结尾的正则字面量/* let regex /[0-9]*/ */这里块注释会在第二行提前结束*/被误认为注释结束符而不是如用户预期地延伸到第三行。这是字符串字面量中已有的问题但正则中*量词的高频出现使问题更易触发。规避方式改用行注释//Xcode 注释多行代码时使用的正是行注释语法。与中缀运算符的歧义正则字面量与中缀运算符连写时存在轻微歧义无空格连写如x/y/会被解析为使用中缀运算符/。因此需要空格分隔如x /y/或改用扩展字面量x#/y/#。/.../的语法限制与解析规则首尾字符限制为避免解析歧义/.../正则字面量不能以空格或制表符开头或结尾。该限制可通过扩展字面量#/.../#规避。结尾限制的理由避免破坏特定场景下前缀/中缀/运算符的源码兼容性下文详述。开头限制的理由当/.../正则字面量位于行首时会引发解析歧义这对结果构建器场景尤其麻烦——正是正则字面量预期被频繁使用的地方let digit Regex { TryCapture(OneOrMore(.digit)) { Int($0) } } // Matches against digit ( | - ) digit let regex Regex { digit / [-] / digit }如果不加限制上述代码会被解析为操作数为digit、[-]、digit的单一运算符链而非三个结果构建器元素从而被诊断为语义无效。规避方式转义首个空格/\ [-] /或使用扩展字面量#/ [-] /#。该限制利用了中缀运算符要求两侧空白一致空格与换行均算空白的性质let a 0 1 // Valid let b 01 // Also valid let c 0 1 // Valid operator chain because the newline before is whitespace. let d 0 1 // Not valid, is treated as prefix, which cannot then appear next to 0. let e 0 1 // Same but postfix let f 0 1 // Not a valid operator chain, same as d, except 1 is no longer sequenced with 0.与f同理要求正则首字符非空格/制表符后下面代码必然被解析为正则let g 0 /1 2/ // Must be a regex表达式位置的解析规则/.../正则字面量在表达式位置遇到开/且存在闭/时被解析。因此以下代码继续按常规解析// Infix / is never in an expression position in valid code (unless unapplied). let a 1 / 2 / 3 // None of these /^/ cases are in expression position. infix operator /^/ func /^/ (lhs: Int, rhs: Int) - Int { 0 } let b 0 /^/ 1 // Also fine. prefix operator / prefix func / (_ x: Int) - Int { x } let c /0 // No closing /, so not a regex literal. The // of this comment doesnt count either.但let r /^/会被解析为正则。正则字面量可与前缀运算符组合例如let r ^^/x/解析为let r ^^(/x/)——遇到包含/的运算符字符处于表达式位置时/之前的字符被拆分为前缀运算符随后按正则字面量继续解析。不平衡括号启发式首尾非空白的限制不足以消歧全部情况例如// Prefix / used multiple times on the same line without trailing whitespace: (/x).foo(/y) bar(/x) bar(/y) // Cases where the closing / is not used with whitespace: bar(/x)/2 baz(!/, 1)/2 // Prefix /^ with postfix /: let f (/^x)/这些场景中开/都在表达式位置且存在未使用空白的潜在闭/。为避免源码破坏提案采用进一步的启发式若字面量内含有不平衡的)则不将其解析为正则。该判断考虑转义与自定义字符类因此只作用于本身对正则非法的情况上述代码全部继续按常规解析。这一启发式同时也让合法正则场景得到直接消歧foo(/a, b/) // Will become regex literal /a, b/ qux(/, !/) // Will become regex literal /, !/ qux(/,/) // Will become regex literal /,/ let g hasSubscript[/]/2 // Will become regex literal /]/ let h /0; let f 1/ // Will become the regex literal /0; let y 1/ let i /^x/ // Will become the regex literal /^x/可通过插入括号或空白恢复原有语义// Now a prefix and postfix /: foo((/a), b/) // Now unapplied operators: qux((/), !/) qux((/),/) let g hasSubscript[(/)]/2 let h (/0); let f 1/ // Now prefix / and postfix / let i (/^x)/ // Now prefix /^ and postfix /qux(/, /) let g hasSubscript[/] / 2特殊的边缘情形是双/未应用中缀运算符例如baz(/^/)会变成正则字面量/^/而非未应用运算符。它无法用括号或空白消歧但可用闭包消歧baz({ $0 /^/ $1 }) // Is now infix /^/这利用了正则字面量不会在中缀运算符位置被解析的性质。源码兼容性与启用方式/.../解析在同时满足以下条件时可能破坏源码/出现在表达式位置同一行存在闭/字面量首尾字符不是空格或制表符字面量内没有不平衡的)字符。提案预计这些情况不常见且可用括号或闭包消歧。为容纳可能被破坏的源码/.../正则字面量在 Swift 6 语言模式中启用希望提前使用的项目可传入编译器标志-enable-bare-slash-regex或启用 SE-0362 定义的 upcoming feature flagBareSlashRegexLiteralsSwift 5.8 实现。注意这不影响扩展定界符#/.../#——它立即可用无需任何标志。在 SwiftPM 中可通过SwiftSetting.enableUpcomingFeature(BareSlashRegexLiterals)按 target 启用并使用#if compiler(5.7) hasFeature(BareSlashRegexLiterals)做特性探测以兼容旧工具链。未来方向更现代的语法未来可支持更 Swift 化的正则字面量语法如用//、/* ... */写注释、用...写引号序列。但这与打算解析的正则语法超集不兼容可能需要引入新字面量种类且没有明显的定界符选择。此外这种语法会失去标准正则的熟悉感可能带来“恐怖谷”效应。重复命名组的类型化捕获PCRE 在设置(?J)时允许重复的捕获组名但这与带标签的元组元素不兼容元组不允许重名。目前字面量不支持(?J)此场景留作未来工作。分支重置交替的类型化捕获PCRE 与 Perl 支持(?|(a)|(b))分支重置构造使子交替重置各分支的捕获编号让(a)与(b)共享同一捕获号。这会要求统一它们的类型。目前不支持该构造留作未来工作。库可扩展的协议支持正则字面量描述的是可在某种字符串模型上运行的字符串处理算法基于扩展字素簇还是 Unicode 标量值的语义属于 SE-0350 的 Unicode 讨论。现有ExpressibleBy*协议方案力不从心因为库需要访问算法本身的结构。更好的未来方向是开放正则解析器的 AST、API 与 AST actions 给库典型用途包括支持本地化比较或定制字素簇断点等更高层字符串模型支持对接 ICU、PCRE、JavaScript 等其他引擎将 Swift 正则语法美化打印为对应方言在字面量外层构建多级处理结构如 URL 处理中的百分号编码映射。备选方案回顾提案对定界符选择进行了详尽权衡仅保留扩展定界符#/.../#可避免歧义与源码破坏但简单正则中#的额外噪音不值得前缀引号re...两字母前缀可作为未来字面量类型的命名空间扩展为re#...#与re...也自然但单引号与正则元字符冲突如(?name)、\gname、\kname、(?Carg)且易与原始字符串混淆前缀双引号re....单引号正则语法可直接使用但正则字面量更接近“程序字面量”而非“数据字面量”单引号有助于表达这种差异单字母前缀r...更简洁但易被误解为“raw”Python 正是用此表示原始字符串单引号...最简洁但与字符串字面量语法过于接近语义不明魔法字面量#regex(...)需正确平衡括号否则整行被误判加重认知负担#regex(/.../)则更笨重该方案对#literal(...)语法一致性也有破坏缩短魔法字面量#(...)仍保留上述问题且无理由让正则享有特权语法双斜杠// ... //与注释冲突文件头注释如// rdar://41219750将大面积受损编辑器也无法自动补全闭定界符复用字符串字面量语法无法获得正则专属语法高亮、需要Regex上下文类型否则默认String、重载场景语义模糊、正则专属转义\w需原始字符串语法、与插值不兼容无自定义字面量try! Regex([abc])丧失全部编译期优势——无工具支持、错误在运行时诊断、失去类型安全、语法更冗长。其他被否决的选项还包括单行字面量默认无语义空白会失去熟悉度与兼容性可显式(?x)开启多行字面量默认语义空白需空白剥离规则且(?x)前置写法冗长、受缩进影响定界符携带完整匹配选项如#/(?xi)过于冗长且词法复杂度高/.../x后缀选项运行时匹配选项用/.../.ignoresCase()API 暴露解析类选项x、xx、n、J分别由多行字面量与(?n)、(?J)覆盖///或#///多行定界符与文档注释冲突不支持多行字面量以及把特性集限制到 DSL 子集会删除命名捕获标签这一体现元组标签类型操作的清晰示例。实战建议小结首选/.../模式简单、捕获少时优先使用Swift 5.7 语言模式起直接可用需要内部斜杠用#/.../#避免\/转义噪音反斜杠仍保持正则语义复杂模式用多行字面量开定界符后紧跟换行即启用(?x)扩展语法可用# comment注释闭定界符/#必须位于新行警惕注释与运算符冲突块注释中避免出现以*结尾的正则与中缀运算符连写需空格分隔或使用扩展定界符字面量不以空格/制表符开头或结尾提前在 Swift 5.x 使用/.../传入-enable-bare-slash-regex或通过BareSlashRegexLiteralsupcoming feature flag 启用与 DSL 混用用Regex { ... }包裹字面量让简洁语法与强类型转换TryCapturetransform各司其职。本指南基于仓库中的 SE-0354 提案原文其配套的正则语法定义见 SE-0355类型与 API 总览见 SE-0350DSL 见 SE-0351特性开关机制见 SE-0362读者可据此深入研读。【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolution创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表