Swift 抛砖引玉:从数组访问越界想到的“可抛出错误”属性

Swift 抛砖引玉:从数组访问越界想到的“可抛出错误”属性_第1张图片

0. 概览

了解 Swift 语言的小伙伴们都知道,我们可以很方便的写一个可能抛出错误的方法。不过大家可能不知道的是在 Swift 中对于结构或类的实例属性我们也可以让它抛出错误。

这称之为实效只读属性(Effectful Read-only Properties)。

那么,这种属性怎么创建?并且到底有什么用处呢?

在本篇博文中,您将学到如下内容

  • 0. 概览
  • 1. 什么是“实效只读属性”
  • 2. 怎么创建“实效只读属性”?
  • 3. 数组访问越界是个头疼的问题
  • 4. 拯救者:抛出错误的“实效只读属性”
  • 5. 更进一步
  • 6. 八卦一下:ruby 中更优雅的实现
  • 总结

相信看完文本后,小伙伴们的武器库中又会多了一件“杀手锏”!

那还等什么呢?Let‘s go!!!


1. 什么是“实效只读属性”

“实效只读属性” 英文名称为 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)中观赏更详细的内容:

  • SE-0310:Effectful Read-only Properties

2. 怎么创建“实效只读属性”?

从 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 }
  //            ^~~~~~~~~
  //            同样的,下标读操作也是异步的
}

3. 数组访问越界是个头疼的问题

秃头码农们都知道,在 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 一切开始变得不同了。

4. 拯救者:抛出错误的“实效只读属性”

看到这,聪明的小伙伴们应该早就知道如何应对了。

我们可以使用 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 语言中实现的是何其优雅。

5. 更进一步

在数组的下标访问中抛出错误还不算完,我们还可以利用 Swift 枚举的关联类型为错误添加进一步的信息。


想要了解更多 Swift 枚举的小秘密,请小伙伴们移步如下文章观赏:

  • Swift 和 Python 两种语言中带关联信息错误(异常)类型的比较

更多 Swift 语言知识的系统介绍,请移步我的专栏文章进一步观看:

  • Swift 语言开发精讲(文章平均质量分 97)

下面,我们为之前的越界错误增加关联类型,分别表示当前越界的索引和数组总长度:

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 抛砖引玉:从数组访问越界想到的“可抛出错误”属性_第2张图片

6. 八卦一下:ruby 中更优雅的实现

上面我们提到过 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 代码举了一个更优雅的实现。

感谢观赏,再会!

你可能感兴趣的:(Apple开发入门,极客,swift,数组访问越界,Effectful,Properties,属性,subscript,枚举关联类型)