ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Swift SE-0310 深度解读:Effectful Read-only Properties——让计算属性与下标支持 async 与 throws

Swift SE-0310 深度解读:Effectful Read-only Properties——让计算属性与下标支持 async 与 throws 文档【免费下载链接】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-0310: Effectful Read-only Properties 为骨架系统讲解如何在 Swift 中为**只读计算属性read-only computed property与只读下标read-only subscript**声明async、throws可二者兼有等效果effect修饰符。该特性于 Swift 5.5 落地与 SE-0296 async/await、actors、结构化并发 共同构成 Swift 并发编程的基础设施。读完本文你将掌握get async throws的完整语法、访问端await/try的静态强制规则、协议一致性witness 效果子集约束、类继承覆写规则、以及 Objective-C 完成处理器方法通过swift_async_name(getter:...)注解导入为 effectful 属性的机制并理解为什么可写属性和 KeyPath 被刻意排除在本提案之外。背景为什么需要“带效果”的属性Swift 的计算属性computed property与下标subscript是“在 get/set 时执行程序员指定计算的成员”。SE-0296 引入了async/await但并未说明计算属性或下标能否携带异步等效果。同时为了让属性能够利用async支持throws也同样重要——本提案的目标就是为只读计算属性与下标补上这一块语法与语义。提案首先澄清了术语与前提只读计算属性仅定义get访问器的计算属性只读下标同理。提案后续所有不加限定的“property”“subscript”均指只读版本效果effect函数的可观察行为Swift 类型系统跟踪三类效果——throws可能沿异常失败路径以Error返回、rethrows传入的抛错闭包可能被调用、async可能到达挂起点文中示例大量借用结构化并发与 actor 特性阅读前提是已基本理解这些并发原语的重要性。动机同步上下文中并发能力的锁死Swift 并发场景属性无法 await异步调用不能出现在同步上下文中这一根本限制意味着计算属性与下标在使用 Swift 新并发特性时严重受限——它们唯一的并发手段是创建 detached task但无法在同步上下文中await其完成以得到答案class Socket { // ... public var alive: Bool { get { let handle detach { await self.checkSocketStatus() } return await handle.get() // ^~~~~ error: cannot await in a sync context } } private func checkSocketStatus() async - Bool { /* ... */ } }如果属性能声明为asyncalive就能直接await checkSocketStatus()的结果。类似地想要利用 actor 隔离并发资源、同时通过属性对外暴露信息也是不可能的——因为从 actor 隔离上下文之外与其交互必须awaitstruct Transaction { /* ... */ } enum BankError: Error { /* ... */ } actor AccountManager { // NOTE: getLastTransaction is viewed as async // when called from outside of the actor func getLastTransaction() - Transaction { /* ... */ } func getTransactions(onDay: Date) async - [Transaction] { /* ... */ } } class BankAccount { // ... private let manager: AccountManager? var lastTransaction: Transaction { get { guard let manager manager else { throw BankError.NoManager // ^~~~~ error: cannot throw in a non-throwing context } return await manager.getLastTransaction() // ^~~~~ error: cannot await in a sync context } } subscript(_ d: Date) - [Transaction] { return await manager?.getTransactions(onDay: d) ?? [] // ^~~~~ error: cannot await in a sync context } }lastTransaction中throw的用法还暴露出另一个缺失当前属性若想表达失败只能返回OptionalTransaction或结构相似的enum/元组。有了throw属性就能以 Swift 既有的错误处理机制向使用者描述“哪里出了问题”而不是简单地返回nil。值得强调的是计算属性 getter 语法上无法接受显式参数如完成处理器这是其与方法的本质差异之一而async函数的出现使得“无需显式完成处理器参数”也能表达异步因此async计算属性并不违背属性访问的既有语法——这主要是一个类型系统层面的区分。既有代码阻塞且可失败的属性Swift API 设计指南提醒Document the complexity of any computed property that is not O(1).People often assume that property access involves no significant computation, because they have stored properties as a mental model. Be sure to alert them when that assumption may be violated.但实践中确实存在可能阻塞或失败的属性。现实世界的例证是 SDK 中的AVAsynchronousKeyValueLoading协议——它专门用来查询类型属性状态并提供异步加载机制AVAsset等类型依赖它正是因为其只读属性是阻塞且可失败的。将AVAsynchronousKeyValueLoading解决的问题提炼成简单示例由于get无法接收完成处理器既有代码只能另写一个异步版本方法作为变通class NetworkResource { var isAvailable: Bool { get { /* a possibly blocking operation */ } } func isAvailableAsync(completionHandler: ((Bool) - Void)?) { // method that returns without blocking. // completionHandler is invoked once operation completes. } }问题在于即使给isAvailable加了“get 可能阻塞”的注释程序员仍可能误用同步版本因为注释很容易被忽略。但如果isAvailable的get被标记为async类型系统就会强制使用await从而在编译期告知使用者“该属性访问可能挂起直到操作完成”。可见async效果修饰符是用类型检查器强化了 API 设计指南的建议。解决方案在get上声明效果修饰符核心语法解决方案非常直接允许在只读计算属性或下标的get访问器上标注async、throws或两者class BankAccount { // ... var lastTransaction: Transaction { get async throws { // -- NEW: effects specifiers! guard manager ! nil else { throw BankError.notInYourFavor } return await manager!.getLastTransaction() } } subscript(_ day: Date) - [Transaction] { get async { // -- NEW: effects specifiers! return await manager?.getTransactions(onDay: day) ?? [] } } }访问这些属性时表达式会被视为带有 getter 上列出的效果按需用await或try包起来extension BankAccount { func meetsTransactionLimit(_ limit: Amount) async - Bool { return try! await self.lastTransaction.amount limit // ^~~~~~~~~~~~~~~~ // this access is async throws } } func hadWithdrawlOn(_ day: Date, from acct: BankAccount) async - Bool { return await !acct[day].allSatisfy { $0.amount Amount.zero } // ^~~~~~~~~ // this access is async }关键限制计算属性或下标只有在定义的访问器仅有get时才支持效果修饰符。这个只读限制的主要目的在于把本提案的范围控制在简单、有用、易懂的特性上且不妨碍未来提案为可变成员提供效果支持可写成员为何棘手见“Extensions considered”一节。详细设计语法与语义依据《The Swift Programming Language》中 “Type Variable Properties” 文法本提案的修改与新增如下getter-clause → attributes? mutation-modifier? get getter-effects? code-block getter-effects → throws getter-effects → async throws?其中getter-effects是新增文法产生式允许get与{之间出现三种效果组合之一并强制async在throws之前——与既有函数上的顺序一致。此外可以在协议中声明而非定义effectful 属性getter-setter-keyword-block → { getter-keyword-clause setter-keyword-clause? } getter-setter-keyword-block → { setter-keyword-clause getter-keyword-clause } getter-keyword-clause → attributes? mutation-modifier? get getter-effects?例如protocol Account { associatedtype Transaction var lastTransaction: Transaction { get async throws } subscript(_ day: Date) - [Transaction] { get async } }这强制要求遵循Account协议的类型提供效果相同或更少的属性/下标见证witness。对 effectful 属性定义的解释很直接get定义中的code-block允许表现所声明的效果可 throw、可挂起从而在其中使用await与try表达式而对属性或下标的访问表达式则被视为携带声明在该属性上的效果。因为属性的声明在静态上总是可知的所以总能判定属性是否有效果——漏写相应的await、try属于静态错误。协议一致性Protocol Conformance类型若想遵循包含 effectful 属性的协议其属性或下标必须展现与协议要求相同或更少的效果。这与函数效果的一致性检查规则一致witness 可以缺少某个效果但不能表现出要求之外的效果。一个完全良类型、无冗余await/try的例子protocol P { var someProp: Int { get async throws } } class NoEffects: P { var someProp: Int { get { 1 } } } class JustAsync: P { var someProp: Int { get async { 2 } } } struct JustThrows: P { var someProp: Int { get throws { 3 } } } struct Everything: P { var someProp: Int { get async throws { 4 } } } func exampleExpressions() async throws { let _ NoEffects().someProp let _ try! await (NoEffects() as P).someProp let _ await JustAsync().someProp let _ try! await (JustAsync() as P).someProp let _ try! JustThrows().someProp let _ try! await (JustThrows() as P).someProp let _ try! await Everything().someProp let _ try! await (Everything() as P).someProp }形式上设 getterG关联的效果集合为effects(G)本提案为一致性检查新增一条规则——若 getter 定义W满足协议 getter 声明R的要求则effects(W)必须是effects(R)的子集。类继承Class InheritanceEffectful 属性与下标可以继承自基类遵循通常的可见性规则。关键差异在于覆写继承来的 effectful 属性或下标时子类属性必须具有相同或更少的效果。这是类子类型关系的自然推论——基类必须能涵盖其子类可能展现的全部效果。本质上这条规则与协议一致性规则相同。Objective-C 桥接希望利用 effectful 属性的 API 设计者可以将 Objective-C 方法导入为 Swift 属性。Objective-C 方法通常导入为 Swift 方法因此通过显式注解opt-in控制其导入为 effectful 属性避免对已导入声明造成源码兼容性问题。由于 Swift 属性的只读限制以及大量可失败的 Objective-C 方法已按throws方法导入本提案的 Objective-C 桥接范围限定为 Swift 并发特性不支持导入为 effectful 下标向 Objective-C 导出 effectful 属性也留给未来工作。要导入一个 Objective-C 方法为 Swift effectful 属性该方法必须符合 SE-0297 所述asyncSwift 方法的导入规则即该方法具有可由swift_async约定识别的完成处理器参数。随后注解将该导入行为改为生成 effectful Swift 计算属性而非asyncSwift 方法原始 ObjC 方法仍会作为普通 Swift 方法一并导入。具体而言满足以下条件的 Objective-C 方法恰好接受一个参数——完成处理器且可被 SE-0297 的约定识别方法返回void方法标注了__attribute__((swift_async_name(getter:myProp())))——注意getter:前缀用于指定应导入为属性而非方法将被导入为名为myProp的 effectful 只读 Swift 属性。以下是 SDK 中已按此注解的示例// from Safari Services interface SFSafariTab: NSObject - (void)getPagesWithCompletionHandler:(void (^)(NSArraySFSafariPage * *pages))completionHandler __attribute__((swift_async_name(getter:pages()))); // ... end // from Exposure Notification interface ENManager: NSObject - (void)getUserTraveledWithCompletionHandler:(void (^)(BOOL traveled, NSError *error))completionHandler __attribute__((swift_async_name(getter:userTraveled())));; // ... end导入到 Swift 后形如class SFSafariTab: NSObject { var pages: [SFSafariPage] { get async { /* ... */ } } // ... } class ENManager: NSObject { var userTraveled: Bool { get async throws { /* ... */ } } }作为背景补充SE-0297 定义了完整的swift_async属性族swift_async(none)禁用转换、swift_async(not_swift_private, C)指定完成处理器参数索引、swift_async_name(...)指定导入名、swift_async_error(...)控制NSError *如何映射为throws并给出了完成处理器推断启发式参数名匹配completion、withCompletion、completionHandler、replyTo等。SE-0310 的getter:约定正是在此基础上的进一步扩展。兼容性影响源码兼容性Source compatibility本提案的语法变更若出现在旧版语言中会被解析器作为错误拒绝——因此不破坏既有源码。ABI 稳定性ABI stability本提案是增量的并有意收窄范围以避免破坏 ABI。API 弹性API resilience作为增量特性不影响 API 弹性但采用effectful 只读属性的既有 API 会破坏向后兼容——API 的使用者必须用await和/或try包裹属性访问。被排除的扩展Extensions consideredEffectful 可写属性定义 async/或 throwing 可写属性与以下特性之间的交互是一个大工程需要大量实现投入inout_modify属性观察器didSet、willSet属性包装器property wrappers可写下标本提案的首要动机是让 Swift 并发特性可用于计算属性与下标只读 effectful 属性的设计小而直白、实现简单却能给真实程序带来显著收益。Key PathsKey-path 表达式是KeyPath类及其类型擦除变体的语法糖。引入 effectful 属性需要修改各类型的subscript(keyPath:)合成还很可能需要限制能访问 effectful 属性的 key-path 的类型擦除。原因在于Swift 不允许仅凭效果差异重载函数因此需要某种类似rethrows的机制以及async的对应物“reasync”作为subscript(keyPath:)的起点虽然 key-path 字面量可自动视为函数但一般的KeyPath值不是函数其类型无法携带效果这使得“可 rethrows 的subscript(keyPath:)”难以成立也可引入带各种能力的新 key-path 种类类似现有WritableKeyPath、ReferenceWritableKeyPath并为“无效果”之外的三种效果组合各合成带正确效果修饰符的subscript版本例如subscriptT: ThrowingKeyPath(keyPath: T) throws——但这需要对类型系统做非平凡重构或大幅扩展KeyPathAPI。因此本提案暂禁止通过 key-path 访问 effectful 属性。既有的 key-path 到可变属性的限制依据实例类型如WritableKeyPath说明这种限制并不罕见。值得留意的是SE-0479: Method and Initializer Key Paths 在后续演进中依然明确将 effectful 值类型排除在外——其文档指出“effectful 值类型全部需要新的KeyPath类型故未纳入该提案”并希望由一个统一提案在全部 key-path 组件种类上解决这一缺口印证了 SE-0310 当初的取舍。效果修饰符的位置Alternatives considered提案还系统比较了效果修饰符的四种候选位置A var prop: Type B { C get D { } }位置 A主要被private(set)等访问修饰符或override等声明修饰符占用更“效果化”的mutating/nonmutating只允许出现在位置 C。override async throws var prop或async throws override var prop读起来不佳故弃用位置 B效果只作为函数类型的一部分而非其他类型Int async throws会让人误以为是类型引入新标点也被否决位置 C只被mutating/nonmutating占用但与函数上“效果置于主语之后”的惯例不一致既然位置 D 可用位置 D 更合理位置 D最终选择这是文法中未占用的位置将效果置于访问器而非变量或其类型上且与函数声明中效果置于主语之后一致——get throws、get async throws中get即主语。另一好处是远离变量避免混淆访问器的效果与被返回函数的效果var predicate: (Int) async throws - Bool { get throws { /* ... */ } }访问predicate可能 throw若未 throw得到的是一个 async throws 函数。同时提案承认隐式 getter 简写无法承载效果修饰符var predicate: (Int) async throws - Bool { /* ... */ }因为简写是语法糖必然以简洁换灵活性因此 effectful 属性不能用简写声明必须使用显式定义访问器的完整语法。下标为何不用“方法式”位置下标的主要差异在于方法式的头部语法与隐式 getter 简写支持使其看起来像方法class C { subscript(_ : InType) E - RetType { /* ... */ } }位置 E 看似诱人但下标不是方法不能以c.subscript作为一等函数值访问也不能用c.subscript(0)调用只能以索引语法c[0]访问方法不可赋值下标索引表达式却可以。因此下标更接近“可接受一个参数的属性”。更关键的是若未来可写下标支持效果位置 E 会同时出现在完整语法与简写语法中造成两者不一致而在完整语法中采用位置 E 还会带来令人困惑的写法subscript(_ i : Int) throws - Bool { get async { } set { } }这里唯一的合理解释是set是 throws、get是 async throws——程序员必须在多个位置心算汇总一个访问器允许的效果当get体很长时尤其糟糕。因此位置 D 被选定为唯一权威位置无论下标还是计算属性看一个位置就能确定该访问器是否有效果。其他rethrows被排除在外因为属性get操作期间无法传入闭包或任何显式值async/await是为异步编程量身定做的throws/try同理因此不考虑不依赖这些特性的替代方案。仓库中的下游印证SE-0310 的“只读 effectful”设计在 Swift Evolution 后续提案中被反复引用为既定事实可作为理解其实际影响的旁证SE-0336: Distributed Actor Isolation 明确指出由于distributed关键字的效果化本质分布式计算属性只能支持只读因为“只有只读属性才可能是 effectful由 SE-0310 引入”distributed var size: Int { self.chunk.size }会被隐式视为async throws访问时必须try await chunk.size且无法声明读/写分布式计算属性——这是 SE-0310 只读限制的直接受益与沿袭。SE-0304: Structured Concurrency 的未来方向一节说明SE-0317async let正是“创建异构子任务、并把结果捕获到使用 effectful properties 等待子任务完成的局部变量”的语法糖——async let veggies chopVegetables()的背后就有 effectful 属性的身影。SE-0317: async let 本身也讨论过用“属性包装器 effectful 属性”近似async let行为的方案AsyncLet var veggies try await chopVegetables()但指出属性包装器无法提供结构化并发的语义最终未采用。这些后续提案一方面印证了 SE-0310 的设计边界只读、效果子集约束如何为整个并发模型铺路另一方面也说明其限制KeyPath、可写属性在多年后仍是待解决的开放问题。小结SE-0310 用最小且优雅的语法增量——在get与{之间放置async/throws——为 Swift 的类型系统补上了“效果化属性访问”这一环。它带来的核心价值有三让计算属性与下标能直接参与 Swift 并发模型await actor 隔离资源、表达可能挂起的查询用throws让属性具备与既有错误处理机制一致的失败表达能力并通过静态类型强制漏写await/try即为编译错误把 API 设计指南中“O(1) 属性假设”的提醒升级为编译器强制的契约。与此同时效果子集规则保证了协议一致性与类继承的多态安全性swift_async_name(getter:...)桥接则让既有 Objective-C 异步 API 能以属性形态平滑进入 Swift。理解了本文的语法、语义与边界取舍你就掌握了 Swift 5.5 之后编写“既可 await、又可 throw”的属性与下标的全部要点。赞分享文档【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址https://gitcode.com/gh_mirrors/sw/swift-evolution点击查看免费下载相关推荐SE-0283 深度解读让 Swift 元组真正支持 Equatable、Comparable 与 HashableSE 0283 深度解读让 Swift 元组真正支持 Equatable、Comparable 与 Hashable SE 0283Tuples Confo文档Swift SE-0413 Typed Throws 全面解读用 throws(E) 精确表达函数抛出的错误类型Swift SE 0413 Typed Throws 全面解读用 throws E 精确表达函数抛出的错误类型 导读 本文以 Swift Evolution文档终极Git代码演变分析工具Git-of-theseus实战指南终极Git代码演变分析工具Git of theseus实战指南 Git of theseus是一款强大的Git仓库分析工具能够帮助开发者可视化项目随时间的增文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表