改进Swift中的完成块

Novoda博客 交叉发布

在Objective-C和Swift中,完成模块都是非常熟悉的模式。 这是一个有用的功能,它使我们可以在处理异步调用的同时将方法调用和生成的代码保持在一起。 可以在所有iOS代码中找到完成代码块,例如从URLSession提取的以下示例:

  let task = URLSession.shared.dataTask(with:aUrl,completeHandler:{(data,response,error)in 
//在这里处理结果
})
task.resume()

只要您避免回调地狱,它们是处理方法中单个结果的一种简短且易于编写的方法。 如果您有多个结果或来电,最好使用委托模式。

完成块有问题,但并不总是显而易见的,我们可以通过实现一个实现看到这些:

let task = URLSession.shared.dataTask(with: aUrl, completionHandler: { (data, response, error) in 
if let data = data {
parse(data.asJSON)
} else if let error = error {
display(error)
} else {
// no data and no error... what happened???
}
})
task.resume()

希望现在问题更加明显! 单一责任原则告诉我们“一个类只应有一个改变的理由” ,但它也应适用于职能。 对于此功能,它会执行多项操作-首先确定响应是否成功,然后根据该结果执行操作。 不仅如此,我们还应尽量避免使用条件或将其抽象化。 因此,我们知道应该将这些完成区块分开。

Swift的类型系统使另一个问题更加明显。 看一下完成块的签名:

(Data?, URLResponse?, Error?) -> Void

因此,当请求完成时,我们可能会获得一些数据,我们可能会获得URLResponse,并且可能会出现错误。作为iOS开发人员,按照惯例,我们知道我们将要么获取数据要么获取错误,而不是两者。 但这不是在API中强制执行的; API的设计完全可行,因为它可以同时返回数据错误。 至于URLResponse,您必须深入文档中以查看何时会收到其中之一。

这是一个问题,因为约定只是表示需要隐式知识的另一种方式。 如果您不了解iOS约定,那么您就无法知道该方法在什么条件下会返回什么,而无需谷歌搜索或寻找API文档。 从另一种角度来看,约定是返回某些数据或错误是两个互斥的结果 -如果一个发生,则另一个不会。 但是,API并不表示它们是互斥的-而是通过将所有三个参数都设为可选,API声明我们可以随时获取它们的任意组合! 我们可以消除此代码中的许多歧义,并使其更易于用于此API的客户端。

使用函数重构更好的解决方案

使用有趣的高阶函数概念,我们可以改善许多这些函数的API(URLSession除外,我们将在本文底部解决该问题)。 大多数完成块都带有签名,其中Result可以是任何有用的数据:

(Result?, Error?) -> Void

我们应该针对的是(记住术语“ 互斥” )是两个块,一个块采用以下形式:

resultHandler: (Result) -> Void

另一个形式:

errorHandler: (Error) -> Void

如果您对RxSwift熟悉的话,这些看起来会很熟悉。 要实现包装此功能的东西,我们可以使用泛型:

 func completion(onResult: @escaping (Result) -> Void, onError: @escaping (Error) -> Void) -> ((Result?, Error?) -> Void) { 
return { (maybeResult, maybeError) in
if let result = maybeResult {
onResult(result)
} else if let error = maybeError {
onError(error)
} else {
onError(SplitError.NoResultFound)
}
}
}
 enum SplitError: Error { 
case NoResultFound
}

这个函数创建一个闭包,它将使用两个单独的闭包来处理结果。 这是使用CLGeocoder之前和之后的内容:

 CLGeocoder().geocodeAddressString(location, completionHandler: { [weak self] (maybePlaces, maybeError) in 
if let places = maybePlaces {
self?.handleGeocoding(places: places)
} else if let error = maybeError {
self?.handleError(error: error)
} else {
// what now??
}
})

这是我们的闭包实现:

 CLGeocoder().geocodeAddressString(location, completionHandler: completion( 
onResult: { [weak self] places in
self?.handleGeocoding(places: places)
},
onError: { [weak self] error in
self?.handleError(error: error)
}
))

这样做的好处是,结果不再需要在任何地方处理Optionals,这使我们的代码更直接。 现在的另一个好处是,结果中的两个数据被分成两种不同的情况:一种是请求成功,另一种是失败。 这减少了样板,含糊不清,并且还允许我们使用函数指针来编写简洁易懂的代码:

 CLGeocoder().geocodeAddressString(location, completionHandler: completion( 
onResult: zoomToFirstPlace,
onError: showToast))

重构URLSession

对于URLSession,我们不能使用上面定义的完成函数,因为结果是两个单独的对象— Data和URLResponse。 但是,如果我们认为结果是这两个对象的结合 ,那么我们可以做什么就变得更加清晰。 如果发生错误,并且在这种情况下我们不关心URLResponse,则可以定义一个结构来封装Data和URLResponse:

 struct Response { 
let data: Data
let metadata: URLResponse?
}
 extension URLSession { 
func dataTask(with url: URL, completion: @escaping ((Response?, Error?) -> Void)) -> URLSessionDataTask {
return dataTask(with: url, completionHandler: { (maybeData, maybeResponse, maybeError) in
if let data = maybeData {
completion(Response(data: data, metadata: maybeResponse), nil)
} else if let error = maybeError {
completion(nil, error)
}
})
}
}

然后,这将使我们能够使用我们认为合适的函数:

 URLSession.shared.dataTask(with: aUrl, completion: completion( 
onResult: parseResponseAsJSON,
onError: tryCachedVersion
))

加起来

完成块虽然有用,但还不是很完美。 它们使代码不那么直接,使错误更容易蔓延。将完成块分成单独的数据流,使我们可以更轻松地将我们的快乐之路和错误处理保持分开。 取消对可选变量的处理,使我们可以根据在这种情况下的期望编写更多的代码,而不是根据语言告诉我们必须要做的事情。 Swift的一流函数使我们能够以可维护但易于接近的方式进行操作。

您可以在本要点中找到我们上面用来制作完成框的函数。 如果您还有其他有关完成块的提示,为什么不给我发推文!