了解 Swift 语言的小伙伴们都知道,我们可以很方便的写一个可能抛出错误的方法。不过大家可能不知道的是在 Swift 中对于结构或类的实例属性我们也可以让它抛出错误。
这称之为实效只读属性(Effectful Read-only Properties)。
那么,这种属性怎么创建?并且到底有什么用处呢?
相信看完文本后,小伙伴们的武器库中又会多了一件“杀手锏”!
那还等什么呢?Let‘s go!!!
“实效只读属性” 英文名称为 Effectful Read-only Properties,它是 Swift 5.5+ 中对计算属性和下标操作(computed properties and subscripts)的增强功能。
在 Swift 5.5 之前,我们只能创建异步或可抛出错误的方法(或函数),而无法构建与此类似的实例属性。
对于有些情况,一个“异步”属性可以帮上大忙!
actor AccountManager {
// 注意: `getLastTransaction` 方法若在 AccountManager 外部调用将会“升级”为一个异步方法
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
// ^~~~~ 错误: 普通计算属性中不能抛出错误!
}
return await manager.getLastTransaction()
// ^~~~~ 错误: 普通计算属性中不能调用异步方法
}
}
}
如上代码所示:在 BankAccount 类的 lastTransaction 实例属性访问过程中可能会抛出错误,并且需要等待返回一个异步方法的结果。这对于以往的实例属性来说是“不可能的任务”!
诚然,我们可以将 lastTransaction 实例属性变为一个方法:
class BankAccount {
private let manager: AccountManager?
//var lastTransaction: Transaction {}
func getLastTransaction() async throws -> Transaction {
guard let manager = manager else {
throw BankError.NoManager
}
return await manager.getLastTransaction()
}
}
但这显然有点“画蛇添足”的意味。
幸运的是, 倾听到了秃头码农们的殷切呼唤,从 Swift 5.5 开始我们便有了上面的“实效只读属性”。
想进一步了解“实效只读属性”的小伙伴们可以到 Swift 语言进化提案(swift-evolution proposals)中观赏更详细的内容:
从 Swift 5.5+ 开始,我们可以在实例属性的只读访问器(get)上应用 async 或 throws 关键字(效果说明符):
class BankAccount {
// ...
var lastTransaction: Transaction {
get async throws { // <-- Swift 5.5+: 效果说明符(effects specifiers)!
guard manager != nil else {
throw BankError.notInYourFavor
}
return await manager!.getLastTransaction()
}
}
subscript(_ day: Date) -> [Transaction] {
get async { // <-- Swift 5.5+: 与上面类似,我们也可以在下标的读操作上应用效果说明符。
return await manager?.getTransactions(onDay: day) ?? []
}
}
}
如上代码所示,我们不但可以在实例属性上应用 async 和 throws 效果说明符(effects specifiers),同样也可以在类或结构下标操作的读访问器上使用它们。
现在,我们可以这样访问 BackAccount#lastTransaction 实例属性和下标操作:
extension BankAccount {
func meetsTransactionLimit(_ limit: Amount) async -> Bool {
return try! await self.lastTransaction.amount < limit
// ^~~~~~~~~~~~~~~~
// 对该实例属性的访问是异步且可能抛出错误的!
}
}
func hadWithdrawlOn(_ day: Date, from acct: BankAccount) async -> Bool {
return await !acct[day].allSatisfy { $0.amount >= Amount.zero }
// ^~~~~~~~~
// 同样的,下标读操作也是异步的
}
秃头码农们都知道,在 Swift 中对于数组访问常常出现下标越界的情况。它会引起程序立即崩溃!
我们时常会想:如果在数组访问越界时抛出一个可捕获的错误就好了!
在过去,我们可以写一个新的“下标访问”方法来模拟这一“良好愿望”:
enum Error: Swift.Error {
case outOfRange
}
extension Array where Element: Equatable {
func getElemenet(at: Array.Index) throws -> Element {
guard at < endIndex else {
throw Error.outOfRange
}
return self[at]
}
}
do {
let ary = Array(1...100)
_ = try ary.getElemenet(at: 10000)
} catch let error as Error {
print("ERR: \(error.localizedDescription)")
}
但这种 .getElemenet(at:) 的“丑陋”写法真是让人“是可忍孰不可忍”!
不过,从 Swift 5.5 一切开始变得不同了。
看到这,聪明的小伙伴们应该早就知道如何应对了。
我们可以使用 Swift 5.5 中的“实效只读属性”来“完美的”完成任务:
enum Error: Swift.Error {
case outOfRange
}
extension Array where Element: Equatable {
subscript(index: Array.Index) -> Element {
get throws {
guard index < endIndex else {
throw Error.outOfRange
}
var temp = self
temp.swapAt(0, index)
return temp.first!
}
}
}
do {
let ary = Array(1...100)
_ = try ary[10000]
} catch let error as Error {
print("ERR: \(error.localizedDescription)")
}
如上代码所示:我们使用可抛出错误的下标读访问器为 Array 下标操作“添妆加彩”。略微遗憾的是,我们需要在数组新下标操作中调用原来的下标操作,这对于结构(struct)类型的 Array 来说好似“难于上青天”,所以我们采用的是迂回战术。
对于类支持的类型来说,我们可以使用 Objc 存在的 Swizz 技术来得偿所愿。
在文章最后,我们将会看到同样问题在 ruby 语言中实现的是何其优雅。
在数组的下标访问中抛出错误还不算完,我们还可以利用 Swift 枚举的关联类型为错误添加进一步的信息。
想要了解更多 Swift 枚举的小秘密,请小伙伴们移步如下文章观赏:
更多 Swift 语言知识的系统介绍,请移步我的专栏文章进一步观看:
下面,我们为之前的越界错误增加关联类型,分别表示当前越界的索引和数组总长度:
enum Error: Swift.Error {
case outOfRange(accessing: Int, end: Int)
}
接着,修改抛出错误处的代码:
subscript(index: Array.Index) -> Element {
get throws {
guard index < endIndex else {
throw Error.outOfRange(accessing: index, end: count)
}
var temp = self
temp.swapAt(0, index)
return temp.first!
}
}
最后,是错误捕获时的代码:
do {
let ary = Array(1...100)
_ = try ary[10000]
} catch let error as Error {
if case Error.outOfRange(let accessing, let end) = error {
print("ERR: 数组访问越界[试图访问:\(accessing),数组末尾:\(end)]")
}
}
现在,当发生越界错误时我们可以清楚的知道事情的来龙去脉了,是不是了很赞呢:
上面我们提到过 Swift 结构类型的方法“重载”(结构没有重载之说,这里只是比喻)无法再使用“重载”前的方法了。
但是在某些动态语言中,我们可以非常方便的使用类似于“钩子”机制来访问旧方法,比如 ruby 里:
#!/usr/bin/ruby
class Array
alias :subscript :[]
def [](index)
puts "试图访问索引:#{index}"
subscript(index)
end
end
a = [1,2,3]
puts a[1]
如上所示,我们使用别名(alias)机制将原下标操作方法 :[] 用 :subscript 名称进行“缓存”,然后在新的 :[] 方法中我们可以直接调用旧方法。
运行结果如下所示:
试图访问索引:1
2
Swift 什么时候有这种“神奇”的能力呢?让我们翘首以盼!
在本篇博文中,我们讨论了 Swift 5.5 中新增的“实效只读属性”(Effectful Read-only Properties),它有哪些用途?怎么用它来解决 Swift 数组访问越界的“老问题”?最后,我们用 ruby 代码举了一个更优雅的实现。
感谢观赏,再会!