第38条:为常用的块类型创建 typedef
本条要点:(作者总结)
- 以 typedef 重新定义块类型,可令块变量用起来更加简单。
- 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
- 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应 typedef 中的块签名即可,无须改动其他 typedef。
每个块都具备其“固有类型”(inherent type),因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。例如有下面这个块:
1 ^(BOOL flag, int value) { 2 if (flag) { 3 return value * 5; 4 } else { 5 return value * 10; 6 } 7 8 }
此块接受两个类型分别为 BOOL 及 int 的参数,并返回类型为 int 的值。如果想把它赋给变量,则需要注意其类型。变量类型及相关赋值语句如下:
1 int (^variableName)(BOOL flag, int value) = 2 3 ^(BOOL flag, int value) { 4 // Implementation 5 return someInt; 6 }
这个类型似乎和普通的类型不大相同,然而如果习惯函数指针的话,那么看上去就会觉得眼熟了。块类型的语法结构如下:
return _type(^block_name)(parameters)
与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型起个别名。尤其是打算把代码发布成 API 供他人使用时,更应这样做。开发者可以起个更为易读的名字来表示块的用途,而把块的类型隐藏在其后面。
为了隐藏复杂的块类型,需要用到C 语言中名为 “类型定义”(type definition)的特性。typedef 关键字用于给类型起个易读的别名。比方说,想定义新类型,用以表示接受 BOOL 及 int 参数并返回 int 值的块,可通过下列语句来做:
typedef int(^EOCSomeBlock)(BOOL flag, int value);
声明变量时,要把名称放在类型中间,并在前面加上 “^” 符号,而定义新类型时也得这么做。上面这条语句向系统中新增了一个名为 EOCSomeBlock 的类型。此后,不用再以复杂的块类型来创建变量了,直接使用新类型即可:
1 EOCSomeBlock block = ^(BOOL flag, int value) { 2 3 // Implementation 4 };
这次代码读起来就顺畅多了:与定义其他变量时一样,变量类型在左边,变量名在右边。
通过项特性,可以把使用块的 API 做得更为易用些。类里面有些方法可能需要用块来做参数,比如执行异步任务时所用的 “completion handler” (任务完成后所执行的处理程序)参数就是块,凡遇到这种情况,都可以通过定义别名使代码变得更为易读。比方说,类里有个方法可以启动任务,它接受一个块作为处理程序,在完成任务之后执行这个块。若不定义别名,则方法签名会像下面这样:
1 - (void)startWithCompletionHandler:(void(^)(NSData *data, NSError *error))completion;
注意,定义方法参数所用的块类型语法,又和定义变量时不同。若能把方法签名中的参数类型写成一个词,那读起来就顺口多了。于是,可以给参数类型起个别名,然后使用词名称来定义:
1 typedef void (^EOCCompletionHandler) (NSData *data, NSError *error); 2 3 - (void)startWithCompletionHandler:(EOCCompletionHandler)completion;
现在看上去就简单多了,而且易于理解。当前,优秀的集成开发环境(Integrated Development Environment, IDE)都可以自动把类型定义展开,所以 typedef 这个功能变得很实用。
使用类型定义还有个好处,就是当你打算重构块的类型签名时会很方便。比方说,要给原来的 completion handler 块再加一个参数,用以表示完成任务所花的时间,那么只需要修改类型定义语句即可:
1 typedef void (^EOCCompletionHandler) (NSData *data, NSTimeInterval duration, NSError *error);
修改之后,凡是使用了这个类型定义的地方,比如方法签名等处,都会无法编译,而且报的是同一种错误,于是开发者可据此逐个修复。若不用类型定义,而直接写块类型,那么代码中要修改的地方就更多了。开发者很容易忘掉其中的一两处,从而引发难于排查的 bug。
最好在使用块类型的类中定义这些 typedef,而且还应该把这个类的名字加在由 typedef 所定义的新类型名前面,这样可以阐明块的用途。还可以用 typedef 给同一个块签名类型创建数个别名。在这件事上,多多益善。
Mac OS X 与 iOS 的 Accounts 框架就是个例子。在该框架中找到下面这两个类型定义语句:
1 typedef void (^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error); 2 3 typedef void (^ACAccountStoreRequestAccessCompletionHandler)(BOOL granted, NSError *error);
这两个类型定义的签名相同,但用在不同的地方。开发者看到类型别名及签名中的参数之后,很容易就能理解此类型的用途。他们本来也可以合并成一个 typedef, 比如叫做 ACAccountStoreBooleanCompletionHandler,使用那两个别名的地方,都可以统一使用此名称。然而,这么做之后,块与参数的用途看上去就不那么明显了。
与此相似,如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不能放入同一个继承体系,那么,每个类就应该有自己的 completion handler 类型。这几个 completion handler 的签名也许完全相同,但最好还是在每个类里各自定义一个别名,而不要同一个名称。反之,若这些类能纳入同一个继承中,则应该将类型定义语句放在超类中,以供各子类使用。
END