Swift 在 2.0 版本中引入了结构化错误处理,取代了 Objective-C 的错误指针模式,采用了原生的 throw 和 catch 语义。 rethrows 关键字的出现是为了解决泛型高阶函数(如 map 或 filter)在传递非抛出闭包时,强制调用者使用 try 的特定摩擦,导致不必要的错误处理仪式。
问题的核心在于函数效果多态与子类型化。在 Swift 的类型系统中,非抛出闭包是抛出闭包的子类型,因为它通过从不抛出满足 "可能抛出" 契约。如果没有 rethrows,接受抛出闭包的函数必须无条件传播抛出,强迫所有调用站点无论实际参数的行为如何都必须处理错误。
解决方案是 rethrows 注解,它建立了一个条件契约:函数仅在其闭包参数抛出时抛出。 Swift 编译器通过在编译时跟踪闭包参数的抛出性来实现这一点。当传递非抛出闭包时,函数在调用站点处被视为非抛出,从而消除了 try 的需要;当传递抛出闭包时,函数继承抛出效果。
我们正在为一个 iOS 应用构建一个模块化的数据转换管道,用户可以串联操作,如 JSON 解析、图像调整大小和加密哈希。核心的 pipeline 函数接受定义为 (Data) throws -> Data 的转换数组。起初,我们在 pipeline 上使用标准的 throws 注解,这强迫每个调用站点即使对简单转换也要用 do-catch 块包裹,尽管许多操作是没有失败模式的纯函数。
我们第一种方法重复了整个函数:一个版本命名为 pipeline 用于非抛出转换,另一个版本命名为 pipelineThrowing 用于抛出转换。这样的分离使调用站点清晰,但创造了维护噩梦,每次修复错误都需要编辑两个位置,并且每个新的配置选项都导致 API 表面翻倍。此外,用户必须了解实现细节以选择正确的方法,违反了封装原则。
第二种方法保持了单个 throws 签名,但鼓励使用 try? 来消除警告,有效地丢弃错误信息,并使得在实际错误发生时调试变得不可能。这违反了安全保证,并使代码脆弱,因为开发人员会忘记在包含安全和不安全操作的混合管道中处理真正的错误情况。
最终,我们采用了 rethrows 的解决方案,声明 func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Data。这允许编译器强制在闭包数组包含抛出操作时使用 try,而允许对纯计算直接调用。结果是模板代码减少了 40%,消除了重复的函数签名,并改善了 API 的可用性,其中类型系统准确反映了特定用例的实际错误域。
为什么 Swift 禁止在 rethrows 函数体内直接抛出错误,而是仅允许通过闭包参数抛出?
rethrows 关键字创建了一个严格的透明契约,规定函数仅传播由其参数生成的错误。如果您尝试在函数体内直接 throw CustomError(),Swift 编译器将拒绝它,因为这表示无条件抛出,违反了 "只有在闭包抛出时" 的保证。函数必须处理自己的错误,使用 do-catch,将其转换为返回值,或将签名提升为无条件的 throws,以确保调用者可以安全地假设函数本身不会产生新的错误域。
rethrows 如何与多个闭包参数交互,效果传播有什么影响?
当一个函数有多个标记为抛出的闭包参数,并且函数本身标记为 rethrows 时,只要其中任何一个闭包抛出,函数就会抛出,从而形成效果的并集。Swift 的编译器独立跟踪这些效果,通过调用链,组合的 rethrows 函数保持了条件特性,而无需手动干预。然而,如果您在传递之前变换或包装这些闭包,您必须在包装器中保留抛出签名,否则编译器会将参数视为非抛出,从而导致外部函数失去其条件抛出能力。
rethrows 和 @autoclosure 之间有什么关系,以及为什么这种模式出现在断言 API 中?
@autoclosure 和 rethrows 的组合实现了延迟评估和条件错误传播,在需要时延迟评估,而函数仅在该延迟评估抛出时抛出。这个模式为 Swift 的 assert 和 precondition 函数提供了支持,允许抛出表达式被传递给断言,而无需在断言调用中标记 try。候选人常常遗漏的是,自动闭包必须明确声明 () throws -> T 才能参与 rethrows 契约,并且此机制将评估时机(延迟)与错误传播语义(条件)分开,这对于在发布版本中禁用断言的性能关键代码路径至关重要。