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
可以更好地捕获错误处理,我们异步更新并响应响应。
我们可以通过 @Published
和 ObservableObject
在 Combine 中或通过 delegates
和 didSet
本地处理程序实现反应式更新。无论技术如何,我认为同样的错误处理原则更适合这个问题,并且没有过度设计。
示例代码
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。
我有一个抛出错误的函数,在这个函数中我有一个 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
可以更好地捕获错误处理,我们异步更新并响应响应。
我们可以通过 @Published
和 ObservableObject
在 Combine 中或通过 delegates
和 didSet
本地处理程序实现反应式更新。无论技术如何,我认为同样的错误处理原则更适合这个问题,并且没有过度设计。
示例代码
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。