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


    在这里插入图片描述

    0. 概览

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

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

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

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

    那还等什么呢?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()
          //     ^~~~~ 错误: 普通计算属性中不能调用异步方法
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如上代码所示:在 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()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但这显然有点“画蛇添足”的意味。

    幸运的是, 倾听到了秃头码农们的殷切呼唤,从 Swift 5.5 开始我们便有了上面的“实效只读属性”。


    想进一步了解“实效只读属性”的小伙伴们可以到 Swift 语言进化提案(swift-evolution proposals)中观赏更详细的内容:


    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) ?? []
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    如上代码所示,我们不但可以在实例属性上应用 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 }
      //            ^~~~~~~~~
      //            同样的,下标读操作也是异步的
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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)")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    但这种 .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)")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    如上代码所示:我们使用可抛出错误的下标读访问器为 Array 下标操作“添妆加彩”。略微遗憾的是,我们需要在数组新下标操作中调用原来的下标操作,这对于结构(struct)类型的 Array 来说好似“难于上青天”,所以我们采用的是迂回战术。


    对于类支持的类型来说,我们可以使用 Objc 存在的 Swizz 技术来得偿所愿。


    在文章最后,我们将会看到同样问题在 ruby 语言中实现的是何其优雅。

    5. 更进一步

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


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

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


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

    enum Error: Swift.Error {
        case outOfRange(accessing: Int, end: Int)
    }
    
    • 1
    • 2
    • 3

    接着,修改抛出错误处的代码:

    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!
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    最后,是错误捕获时的代码:

    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)]")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    现在,当发生越界错误时我们可以清楚的知道事情的来龙去脉了,是不是了很赞呢:

    在这里插入图片描述

    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如上所示,我们使用别名(alias)机制将原下标操作方法 :[] 用 :subscript 名称进行“缓存”,然后在新的 :[] 方法中我们可以直接调用旧方法。

    运行结果如下所示:

    试图访问索引:1
    2
    
    • 1
    • 2

    Swift 什么时候有这种“神奇”的能力呢?让我们翘首以盼!

    总结

    在本篇博文中,我们讨论了 Swift 5.5 中新增的“实效只读属性”(Effectful Read-only Properties),它有哪些用途?怎么用它来解决 Swift 数组访问越界的“老问题”?最后,我们用 ruby 代码举了一个更优雅的实现。

    感谢观赏,再会!😎

  • 相关阅读:
    LeetCode 每日一题 2022/11/28-2022/12/4
    使用idea如何打开python项目
    SaaSBase:什么是小店宝?
    DevOps和CI/CD以及在微服务架构中的作用
    阅读类APP广告变现的商业化发展方向
    java前后端分离框架的各自特点是什么?
    Vuex的使用
    拼多多启动第四届农货节:携手10万涉农店铺,与8.8亿消费者共享“秋收喜悦”
    爬虫第四章 ——计算机网络
    redis 分布式锁
  • 原文地址:https://blog.csdn.net/mydo/article/details/134330712