English version also available: TinyExtension: Simplified empty Closure
在Swift语言下,我们经常使用Closure,当然Closure大部分都是用于实现具体功能块,但在一些场景下我们会用到空Closure:
场景一:忽略回调方法
一般来说回调方法都会实现的,但有时候接口提供的功能多于我们的需求,例如我们需要用到下面这一个接口:
func sampleRequest(success: (Bool, String) -> (), failure: () -> ()) {
// some network request logic
// with some local variable store logic
}
一般来说我们都会实现success
和failure
回调,但一些场景下我们确实会忽略一些回调,例如预加载数据的时候,我们不需要处理failure
回调,毕竟预加载都是后台进行,错误信息也不会反馈到用户界面,而且失败也影响不大。我们调用的代码如下:
sampleRequest(success: { (isSuccess, text) in
// do some complex thing
}, failure: {})
这里我们用到了空Closure,其实也很简洁,因为failure
回调没有携带数据,而如果说我们的预加载在接口内都已经完成了,这样success
回调都不需要实现,调用的效果就会变成这样:
sampleRequest(success: { _, _ in }, failure: {})
这时候空Closure就变得没那么简单了,我们必须根据实际的参数数量声明_
去忽略返回的数据。往后如果回调参数数量变化了,我们还需要手动维护这个空Closure。
有人可能说我们可以在sampleRequest()
使用optional的回调配合默认参数nil
去实现忽略回调的效果,但这样我们就需要处理更多optional的内容,而且在一些无法修改接口实现的情况下无法使用。
场景二:Closure回调替代delegate
正如UIKit在Objective-C时代很多类新增了block回调替代简单的delegate,在Swift下因为Closure的使用比Objective-C的block简单很多,我们更常使用Closure回调去实现简单的delegate效果,例如我有三个按钮点击需要通知到其他对象,不使用delegate方式,我们可以声明三个Closure对象:
Class A {
var didTapButtonA: () -> () = {}
var didTapButtonB: (Int, URL?) -> () = { _, _ in }
var didTapButtonC: (Int, Int, Int, Int, URL?) -> () = { _, _, _, _, _ in }
}
然后在按钮触发的时候调用相应的Closure,而需要获取通知的对象只要实现这个属性就可以,就像这样:
let a = A()
a.didTapButtonA = {
// some logic code
}
不过我们可以看到didTapButtonB
和didTapButtonC
已经相当麻烦,问题依然是我们需要配合参数数量去实现空Closure,后续更新也同样需要同步更新。同样我们可以换成optional类型然后把默认值替换成nil
,但同样地我们尽可能避免使用optional。
简化方案
首先我们创建一个名为Closure的struct:
public struct Closure {}
对于忽略回调,我们添加下面的静态方法:
static func ignore() {}
static func ignore(_: Any?) {}
static func ignore(_: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?, _: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?, _: Any?, _: Any?, _: Any?) {}
然后我们上面两个示例调用就可以简化成下面这个效果,我们不需要再管回调了多少个参数,同时其他人也很清晰这部分是我们有意忽略的。
sampleRequest(success: { (isSuccess, text) in
// do some complex thing
}, failure: Closure.ignore)
sampleRequest(success: Closure.ignore, failure: Closure.ignore)
我们没办法实现所有的情况,但6个参数已经能处理大部分的情况,正如Swift的Tuple的Equatable实现,也只实现了6个参数内的情况
而在Closure对象声明的时候,ignore
就不太适合了,所以我选择empty
作为方法名。同时我们需要利用到func
的一个特性,我们可以用同一个名称调用但在不同场景下返回不同的对象。
static func empty() -> (() -> ()) { return {} }
static func empty() -> ((Any?) -> ()) { return { _ in } }
static func empty() -> ((Any?, Any?) -> ()) { return { _, _ in } }
static func empty() -> ((Any?, Any?, Any?) -> ()) { return { _, _, _ in } }
static func empty() -> ((Any?, Any?, Any?, Any?) -> ()) { return { _, _, _, _ in } }
static func empty() -> ((Any?, Any?, Any?, Any?, Any?) -> ()) { return { _, _, _, _, _ in } }
static func empty() -> ((Any?, Any?, Any?, Any?, Any?, Any?) -> ()) { return { _, _, _, _, _, _ in } }
然后声明就可以简化成这个效果:
class A {
var didTapButtonA: () -> () = Closure.empty()
var didTapButtonB: (Int, URL?) -> () = Closure.empty()
var didTapButtonC: (Int, Int, Int, Int, URL?) -> () = Closure.empty()
}
这样我们代码中空Closure的使用就全面统一和简化了,去掉了维护的成本。
其实我们还可以进一步简化,去掉struct Closure
的结构把Closure.ignore
和Closure.empty()
方法抽成全局方法,但全局方法不太符合Swift的模式,同时也容易出现干扰本地变量和方法的情况,因此建议还是保留struct Closure
结构。