第15条:用前缀避免命名空间冲突

  我们在构建应用程序时,可能想将其中部分代码用于后续项目,也可能想把某些代码发布出来,供他人使用。即便现在还不想这么做,将来也总会有用到的时候。如果决定重用代码,那么我们在编写接口时就会将其设计成易于复用的形式。这需要用到 Objective-C 语言中常见的编程范式(paradigm),同时还需了解各种可能碰到的陷阱。

  近年来,开源社区与开源组件随着iOS 开发而流行起来,所以我们经常会在开发自己的应用程序时使用他人所写的代码。与此同时,别人也会用到你的代码,所以,要把代码写得清晰一些,以便其他开发者能够迅速而方便地将其集成到他们的项目里。

  本条要点:(作者总结)

  • 选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀。

  • 若自己所开发的程序库中用到了第三库,则应为其中的名称加上前缀。

  Objective-C 没有其他语言那种内置的命名空间(namespace)机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。如果发生命名冲突(naming clash),那么应用程序的链接过程就会出错,因为其中出现了重复符号:

1 duplicate symbol _OBJC_METACLASS_$_EOCTheClass in:
2         build/something.o
3         build/something_else.o
4 duplicate symbol _OBJC_CLASS_$_EOCTheClass in:
5         build/something.o
6         build/something_else.o

  错误原因在于,应用程序中的两份代码都各自实现了名为 EOCTheClass 的类,这导致 EOCTheClass 所对应的类符号和 “元类”符号各定义了两次。你也许是把两个相互独立的程序库都引入到当前项目中,而它们又恰好有重名的类,所以产生了这一问题。

  比无法链接更糟糕的情况是,在运行期载入了含有重名类的程序库。此时,“动态加载器”(dynamic loader)就遭遇了 “重名符号错误”(duplicate symbol error),就可能会令整个应用程序崩溃。

  避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。所选前缀可以与公司、应用程序或二者皆有关联之名。比方说,假设你所在的公司叫做 Effective Widgets,那么就可以在所有应用程序都会用到的那部分代码中使用 EWS 作前缀,如果有些代码只用于名为 Effective Browser 的浏览器项目中,那就在这部分代码中使用 EWB 作前缀。即便加了前缀,也难保不出现命名冲突,但是其几率会小很多。

  使用 Cocoa 创建应用程序时一定要注意,Apple 宣称其保留使用所有 “两字母前缀”(two-letter prefix)的权利,所以你自己选用的前缀应该是三个字母的。举个例子,加入开发者不遵循这条守则,使用 TW 这两个字母作前缀,那么就会出问题。iOS 5.0 SDK 发布时,包含了 Twitter 框架,此框架就使用 TW 作前缀,其中有个类叫做 TWRequest,它可以发送 HTTP 请求以调用 Twitter API。如果你所在的公司叫做 Tiny Widgets,那么很有可能把访问本公司 API 所用的那个类也命名为 TWRequest。

  不仅是类名,应用程序中的所有名称都应加前缀。如果要为既有类新增 “分类”(category),那么一定要给 “分类”及“分类”中的方法加上前缀,第25条解释了这么做的原因。开发者可能会忽略另外一个容易引发命名冲突的地方,那就是类的实现文件中所用的纯 C 函数及全局变量,这个问题必须要注意。大家可别忘了:在编译好的目标文件中,这些名称是要算作 “顶级符号”(top-level symbol)的。比方说,iOS SDK 的 AudioToolbox 里有个函数能播放声音文件。开发者可向其传入回调函数(callback),以便在播放完毕时调用。你也许想编写一个 Objective-C  类,把这套逻辑封装起来,当播放完声音文件之后,即命令其中的委托对象(delegate)处理回调事宜:

 1 // EOCSoundPlayer.h 
 2 #import <Foundation/Foundation.h>
 3 
 4 
 5 @class EOCSoundPlayer;
 6 @protocol EOCSoundPlayerDelegate <NSObject>
 7 
 8 - (void)soundPlayerDidFinish:(EOCSoundPlayer *)player;
 9 
10 @end
11 
12 @interface EOCSoundPlayer : NSObject
13 
14 @property (nonatomic, weak) id<EOCSoundPlayerDelegate> delegate;
15 - (instancetype)initWithURL:(NSURL *)url;
16 - (void)playSound;
17 
18 @end
19 
20 // EOCSoundPlayer.m
21 #import "EOCSoundPlayer.h"
22 #import <AudioToolbox/AudioToolbox.h>
23 
24 void completion(SystemSoundID ssID, void *clientData) {
25     EOCSoundPlayer *player = (__bridge EOCSoundPlayer *)clientData;
26     if ([player.delegate respondsToSelector:@selector(soundPlayerDidFinish:)]) {
27         [player.delegate soundPlayerDidFinish:player];
28     }
29 }
30 
31 @implementation EOCSoundPlayer {
32     SystemSoundID _systemSoundID;
33 }
34 
35 - (instancetype)initWithURL:(NSURL *)url {
36     if (self == [super init]) {
37         AudioServicesCreateSystemSoundID((__bridge CFURLRef)url, &_systemSoundID);
38     }
39     return self;
40 }
41 
42 - (void)dealloc {
43     AudioServicesDisposeSystemSoundID(_systemSoundID);
44 }
45 
46 - (void)playSound {
47     AudioServicesAddSystemSoundCompletion(_systemSoundID, NULL, NULL, completion, (__bridge void *)self);
48     AudioServicesPlaySystemSound(_systemSoundID);
49 }
50 
51 @end

  这段代码看上去完全正常,不过你再看看该类目标文件中的符号表(symbol table),就会发现问题了:

  符号表中间有个名叫 _completion 的符号,这就是为了处理声音播放完毕之后的逻辑而创建的那个 completion 函数。虽说此函数是在实现文件里定义的,并没有声明于头文件中,不过它仍然算作 “顶级符号”。这样的话,若在别处又创建了一个名叫 completion 的函数,则会于链接时发生类似下面这种 “重复符号错误”:

1     duplicate symbol _completion in:
2         build/EOCSoundPlayer.o
3         build/EOCAnotherCalss.o

  如果将代码发布为程序库,供他人在开发应用程序时使用,那么就更糟糕了。这等于办了件坏事:因为已经有了名叫 _completion 的符号,所以使用此程序库的开发者就无法再创建名为 completion 的函数了。

  由此可见,我们总是应该给这种 C 函数的名字加上前缀。比方说,在刚才那个例子中,播放完声音之后所执行的处理程序可以改名为 EOCSoundPlayerCompletion。这么做还有个好处:若此符号出现在栈回溯信息中,则很容易就能判明问题源自哪块代码。

  如果用第三方库编写自己的代码,并准备将其再发布为程序库供他人开发应用程序所用,那么尤其要注意重复符号问题。你的程序库所包含的那个第三方库也许还会为应用程序本身所引入,若是如此,那就很容易出现重复符号错误了。这时应该给你所用的那一份第三方库代码都加上你自己的前缀。例如,你准备发布的程序库叫做 EOCLibrary,其中引入了名为 XYZLibrary 的第三方库,那么就应该把 XYZLibrary 中的所有名字都冠以 EOC。于是,应用程序就可以随意使用它自己直接引入的那个 XYZLibrary 库了,而不必担心与 EOCLibrary 里的这个 XYZLibrary 相冲突,下图演示了此时的情况:

 

  若应用程序自身和其所用的程序库都引入了同名的第三方库,则后者应加前缀以避免命名冲突

  

  虽说逐个改名是很令人厌烦的事情,不过若想避免命名冲突,还是得费这番工夫才行。读者也许会问:为什么非要这么做呢?应用程序自己不要直接引入 XYZLibrary,改用 EOCLibrary 里面的那个不就行了吗?没错,可以这么做,但是,应用程序也许还会引入另一个名为 ABCLibrary 的第三方库,而该库中又包含了 XYZLibrary。此时,如果你和 ABCLibrary 库的作者都不给各自所用的 XYZLibrary 加前缀,那么应用程序依然会出现重复符号错误。还有一种可能就是,你的库里所用的 XYZLibrary 是 X 版本的,而应用程序却需要使用 Y 版本的某些功能,所以它必须自己再引入一份。你可以花些时间,使用几个流行的第三方库来开发一下iOS 程序程序,那时会经常看到这种前缀的。

  END

posted @ 2017-07-05 00:08  鳄鱼不怕牙医不怕  阅读(525)  评论(0编辑  收藏  举报