Swift 从嵌套在函数中的闭包中抛出

Swift throw from closure nested in a function

我有一个抛出错误的函数,在这个函数中我有一个 inside a 闭包,我需要从它的完成处理程序中抛出错误。这可能吗?

到目前为止,这是我的代码。

enum CalendarEventError: ErrorType {
    case UnAuthorized
    case AccessDenied
    case Failed
}

func insertEventToDefaultCalendar(event :EKEvent) throws {
    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        do {
            try insertEvent(eventStore, event: event)
        } catch {
            throw CalendarEventError.Failed
        }

    case .Denied:
        throw CalendarEventError.AccessDenied

    case .NotDetermined:
        eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
            if granted {
                //insertEvent(eventStore)
            } else {
                //throw CalendarEventError.AccessDenied
            }
        })
    default:
    }
}

在这种情况下这是不可能的 - 必须使用 throws 声明完成处理程序(以及使用 rethrows 的方法)而这个不是。

请注意,所有抛出只是 Objective-C 中 NSError ** 的不同符号(inout 错误参数)。 Objective-C 回调没有 inout 参数,因此无法向上传递错误。

您将不得不使用不同的方法来处理错误。

通常,Obj-C 中的 NSError ** 或 Swift 中的 throws 不能很好地使用异步方法,因为错误处理是同步进行的。

您无法使用 throw 创建函数,但是 return 一个 closure 具有状态或错误! 如果不清楚我可以给一些代码。

当您定义抛出的闭包时:

enum MyError: ErrorType {
    case Failed
}

let closure = {
    throw MyError.Failed
}

那么这个闭包的类型是() throws -> ()并且以这个闭包为参数的函数必须有相同的参数类型:

func myFunction(completion: () throws -> ()) {
}

你可以调用这个函数 completion 闭包同步:

func myFunction(completion: () throws -> ()) throws {
    completion() 
}

并且您必须将 throws 关键字添加到函数签名或调用完成 try!:

func myFunction(completion: () throws -> ()) {
    try! completion() 
}

或异步:

func myFunction(completion: () throws -> ()) {
    dispatch_async(dispatch_get_main_queue(), { try! completion() })
}

在最后一种情况下,您将无法捕获错误。

所以如果 completion 闭包在 eventStore.requestAccessToEntityType 方法中并且方法本身在其签名中没有 throws 或者如果 completion 被异步调用那么你不能 throw 来自这个闭包。

我建议您实现以下函数,将错误传递给回调而不是抛出错误:

func insertEventToDefaultCalendar(event: EKEvent, completion: CalendarEventError? -> ()) {
    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        do {
            try insertEvent(eventStore, event: event)
        } catch {
            completion(CalendarEventError.Failed)
        }

    case .Denied:
        completion(CalendarEventError.AccessDenied)

    case .NotDetermined:
        eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
            if granted {
                //insertEvent(eventStore)
            } else {
                completion(CalendarEventError.AccessDenied)
            }
        })
    default:
    }
}

requestAccessToEntityType 异步执行它的工作。当完成处理程序最终为 运行 时,您的函数已经返回。因此,不可能按照您建议的方式从闭包中抛出错误。

您可能应该重构代码,以便授权部分与事件插入分开处理,并且仅在您知道授权状态为 expected/required.

时才调用 insertEventToDefaultCalendar

如果您真的想在一个函数中处理所有事情,您可以使用信号量(或类似技术),以便异步代码部分与您的函数同步运行。

func insertEventToDefaultCalendar(event :EKEvent) throws {
    var accessGranted: Bool = false

    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        accessGranted = true

    case .Denied, .Restricted:
        accessGranted = false

    case .NotDetermined:
        let semaphore = dispatch_semaphore_create(0)
        eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
            accessGranted = granted
            dispatch_semaphore_signal(semaphore)
        })
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
    }

    if accessGranted {
        do {
            try insertEvent(eventStore, event: event)
        } catch {
            throw CalendarEventError.Failed
        }
    }
    else {
        throw CalendarEventError.AccessDenied
    }
}

因为抛出是同步的,所以想要抛出的异步函数必须有一个抛出的内部闭包,比如这样:

func insertEventToDefaultCalendar(event :EKEvent, completion: (() throws -> Void) -> Void) {
    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        do {
            try insertEvent(eventStore, event: event)
            completion { /*Success*/ }
        } catch {
            completion { throw CalendarEventError.Failed }
        }

        case .Denied:
            completion { throw CalendarEventError.AccessDenied }

        case .NotDetermined:
            eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
                if granted {
                    let _ = try? self.insertEvent(eventStore, event: event)
                    completion { /*Success*/ }
                } else {
                    completion { throw CalendarEventError.AccessDenied }
                }
        })
        default:
            break
    }
}

然后,在调用站点,您可以这样使用它:

   insertEventToDefaultCalendar(EKEvent()) { response in
        do {
            try response()
            // Success
        }
        catch {
            // Error
            print(error)
        }
    }

简单的解决方案

虽然可以使用大量样板通过嵌套闭包来实现这一点,但实现如此简单的事情所需的工作量很大。

以下解决方案并没有从技术上解决问题,而是针对同一问题提供了更合适的设计。

缩小

我相信通过类型实例变量 self.error 可以更好地捕获错误处理,我们异步更新并响应响应。

我们可以通过 @PublishedObservableObject 在 Combine 中或通过 delegatesdidSet 本地处理程序实现反应式更新。无论技术如何,我认为同样的错误处理原则更适合这个问题,并且没有过度设计。

示例代码

class NetworkService {

    weak var delegate: NetworkDelegate? // Use your own custom delegate for responding to errors.

    var error: IdentifiableError { // Use your own custom error type.
        didSet {
            delegate?.handleError(error)
        }
    }

    public func reload() {
        URLSession.shared.dataTask(with: "https://newsapi.org/v2/everything?q=tesla&from=2021-07-28&sortBy=publishedAt&apiKey=API_KEY") { data, response, error in
            do {
                if let error = error { throw error }
                let articles = try JSONDecoder().decode([Article].self, from: data ?? Data())
                DispatchQueue.main.async { self.articles = articles }
            } catch {
                DispatchQueue.main.async { self.error = IdentifiableError(underlying: error) }
            }
        }.resume()
    }
}

备注

我在 Swift 5.5 中写的是 async / await 之前的代码,这使得这个问题变得容易多了。这个答案仍然有助于向后移植 < iOS 13 因为我们需要使用 GCD。