错误处理 - 异步调用

4
我正在为项目创建一个Web服务框架。我已经在GitHub上上传了模板。https://github.com/vivinjeganathan/ErrorHandling 它有各种层。第1层用于验证。第2层用于请求的形成。第3层用于实际的网络调用。
视图控制器<---->第1层<--->第2层<--->第3层
数据通过闭包在层之间流动,如果在任何一层发生错误,它需要优雅地传递到ViewController。
我参考了这个链接来处理异步调用中的错误 - http://appventure.me/2015/06/19/swift-try-catch-asynchronous-closures/ 在同一仓库中创建了一个分支 - 名称 - ErrorHandling-Method1。
我能够将错误从第3层传递到第2层(单级 - 通过闭包中的函数返回响应 - 如链接中所述)。但是,在跨多个层传输时遇到困难。
有人可以协助使用公共GitHub提供的示例应用程序吗?
4个回答

3

我个人建议使用通知的方式,在层中将NSError作为通知的对象,然后在视图控制器中观察该通知。

在层中:

NSNotificationCenter.defaultCenter().postNotificationName("ErrorEncounteredNotification", object: error)

在视图控制器中
NSNotificationCenter.defaultCenter().addObserver(self, selector: "errorEncountered:", name: "ErrorEncounteredNotification", object: nil)

选择器方法:
func errorEncountered(notification: NSNotification!) {
    let error: NSError! = notification.object as! NSError
    NSLog("error: \(error)")
}

你为什么要发布通知而不是捕获抛出的错误呢? - tskulbru

3

你正确地发现了异步代码中错误处理的一个麻烦问题。

对于同步函数,似乎很容易解决——它们只需返回错误代码、具有额外的错误参数或使用新的Swift throws语法。以下是一个同步函数示例:

func computeSome() throws -> Some

这是一个可行的异步函数签名:

func computeSomeAsync(completion: (Some?, NSError?) -> ())

异步函数返回 Void 并不抛出异常。如果失败了,则调用其带有错误参数的完成函数。
然而,完成处理程序很快变得繁琐,特别是在嵌套代码中。
解决方案是使用 Future
func computeSomeAsync() -> Future<Some>

这个函数是异步的,不会抛出异常,并返回一个 Future。那么,什么是 Future?
Future 表示异步函数的最终结果。当你调用异步函数时,它会立即返回并给你一个结果的占位符。这个占位符叫做 future,将由计算值的后台任务最终完成。
当底层任务最终成功时,future 包含函数的计算值。如果失败,它将包含错误信息。
根据实际实现和 Future 库的 API,你可以通过注册 continuations 来获取结果:
let future = computeSomeAsync()

future.onSuccess { value in
    print("Value: \(value)")
}


future.onFailure { error in
    print("Error: \(error)")
}

一开始可能看起来有点奇怪,但是使用 futures 可以做很棒的事情:

fetchUser(id).flatMap { user in
    fetchProfileImage(user.profileImageUrl).flatMap { image in
        cacheImage(image)
    }
}
.onFailure { error in
    print("Something went wrong: \(error)")
}

上述语句是异步的 - 以及函数fetchUserfetchProfileImagecacheImage。包括错误处理。

谢谢。但是我还没有完全理解你的方法(未来函数及其优势)。如果您不介意,能否在Github上修改我的代码,以便我可以进行测试。 - vivin
@vivin 你可以克隆 FutureLib 并阅读 README 文件。在 Xcode 中打开项目并创建一个新的 Playgrounds 文件,尝试在 playground 中复制和理解简单的示例。查看现有的 playground,获取模拟网络请求的示例函数。可能还要看一下 BrightFutures,这是另一个非常棒的 Scala 风格的 futures 和 promises 实现。 - CouchDeveloper

3

首先,我认为没有必要像你这样堆叠层,例如将验证功能作为一个层添加,会增加耦合性,使该层依赖于下面的层(解析、网络等),相反,为什么不将验证分离出来,使其仅依赖于数据?

class ViewController: UIViewController {

   var validator = InputValidator()

   override func viewDidLoad() {
      super.viewDidLoad()

      do {
         try validator.validateInput("INPUT")
         try Fetcher.requestDataWithParams("INPUT")
      }
      catch {
         handleError(error)
      }        
   }
}

现在验证功能不依赖于其他层,通信流程如下:
视图控制器 <---> 解析层 <---> 网络层
我已经重命名了这些层,但它们不一定要像这样,您可以添加或删除层。
如果我尝试解释我的方法,可能会有点复杂,所以我将使用之前的层举个例子,首先是底层:
class NetworkingLayer {
   class func requestData(params: AnyObject, completion: (getResult: () throw -> AnyObject) -> Void) -> Void {
      session.dataTaskWithURL(url) { (data, urlResponse, var error) in
         if let error = error {
            completion(getResult: { throw error })
         } else {
            completion(getResult: { return data })
         }
      }
   }
}

我省略了一些代码部分,但是核心思想是要执行任何必要的步骤使图层正常工作(例如创建会话等),并始终通过完成闭包进行通信;上面的图层看起来像这样:

class ParsingLayer {
   class func requestObject(params: AnyObject, completion: (getObject: () throw -> CustomObject) -> Void) -> Void {
      NetworkingLayer.requestData(params, completion: { (getResult) -> Void in
         do {
            let data = try getResult()
            let object = try self.parseData(data)
            completion(getObject: { return object })
         }
         catch {
            completion(getObject: { throw error })
         }
      })
   } 
} 

请注意,完成闭包不同,因为每个层都添加了功能,返回的对象可能会改变。另外请注意,do语句内部的代码可能会出现两种故障:第一种是网络调用失败,第二种是无法解析来自网络层的数据; 向上层的通信总是通过完成闭包进行的。
最后,ViewController可以使用在此情况下Parsing层期望的闭包来调用下一层,并能够处理任何层中产生的错误。
override func viewDidLoad() {
   super.viewDidLoad()
   do {
      try validator.validateInput("INPUT")
      try ParsingLayer.requestObject("INPUT", completion: { (getObject) in
      do {
         let object = try getObject()
         try self.validator.validateOutput(object)
         print(object)
      }
      catch {
         self.handleError(error)
      }
   })
   catch {
      handleError(error)
   }        
}

请注意,完成闭包中有一个do catch,这是必要的,因为调用是异步进行的,现在响应已经通过所有层,并实际上已更改为更专业的类型,您甚至可以验证结果而不必需要为验证功能创建一个层。
希望能对您有所帮助。

在这种情况下,解析层依赖于网络层,通信具有1对1的关系,真的没有必要使用广播系统,使用通知的一些缺点包括: -没有编译时间检查以确保观察者正确处理通知。 -不太可追踪(难以调试) -增加了另一层复杂性 -通知名称和UserInfo字典键需要被观察者和控制器都知道 -由于每个人都可以订阅通知,因此数据的封装可能很差 - juanjo
处理程序可能看起来很复杂,但只是在一开始看和理解时比较复杂,在最后它不过是包含另一个闭包的闭包,因为每个层级都接收一个闭包(作为引用传递),所以实际上没有太多的开销,并且每个层级只有两个调用:首先,您调用完成以发送带有结果或错误的闭包,然后下一层调用该闭包以接收结果或错误;另一方面,您使用结果或错误调用通知中心,NC执行搜索并向侦听器(层)发出另一个调用。 - juanjo
此外,我不确定,但我认为使用通知会导致系统内聚性低。 - juanjo
谢谢。我已经被说服继续使用你的实现方案。 - vivin
@vivin 将对象封装到函数中是一个有趣的想法,但您应该意识到由异步函数计算的数据对象通常需要传递到不同的执行环境。这意味着,如果您关心“线程安全”,这种方法存在问题:只有当所有完成处理程序将在与调用站点相同的执行环境中执行时,它才是线程安全的。通常情况下,这并不是这种情况。 - CouchDeveloper
显示剩余8条评论

2
为什么声明方法会抛出异常,但你从未抛出或者尝试捕获呢?你可以通过在所有层次上使用可抛出声明来抛出错误,甚至可以在每个级别上更改可抛出类型。
更新:没有考虑到在异步操作中无法抛出异常。使用 NSNotification 是一个好的选择,或者您可以看一下 RXSwift 或类似的解决方案。我个人推荐使用 RxSwift。这样可以避免你陷入回调地狱中。

这是不可能的,如果你的闭包是异步的 ;) - CouchDeveloper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接