我们在构建应用程序时,可能想将其中部分代码用于后续项目,也可能想把某些代码发布出来,供他人使用。即便现在还不想这么做,将来也总会有用到的时候。如果决定重用代码,那么我们在编写接口时就会将其设计成易于复用的形式。这需要用到Objective-C语言中常见的编程范式(paradigm),同时还需了解各种可能碰到的陷阱。
近年来,开源社区与开源组件随着iOS开发而流行起来,所以我们经常会在开发自己的应用程序时使用他人所写的代码。与此同时,别人也会用到你的代码,所以,要把代码写的清晰一些,以便其他开发者能够迅速而方便地将其集成到他们的项目里。没准会有成千上万个应用程序使用你所写的程序库呢。
Objective-C没有其他语言那种内置的命名空间(namespace)机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。如果发生命名冲突(naming clash),那么应用程序的链接过程就会出错,因为其中出现了重复符号:
duplicate symbol _OBJC_METACLASS_$_EOCTheClass in:
build/something.o
build/something_else.o
duplicate symbol _OBJC_CLASS_$_EOCTheClass in:
build/something.o
build/something_else.o
错误原因在于,应用程序中的两份代码都各自实现了名为EOCTheClass的类,这导致EOCTheClass所对应的类符号和"元类"符号各定义了两次。你也许是把两个相互独立的程序库都引入到当前项目中,而它们又恰好有重名的类,所以产生了这一问题。
比无法链接更糟糕的情况是,在运行期载入了含有重名类的程序库。此时,"动态加载器"(dynamic loader)就遭遇了"重名符号错误"(duplicate symbol error),很可能会令整个应用程序崩溃。
避免此问题的唯一办法就是变相实现命名空间: 为所有名称都加上适当前缀。所选前缀可以使公司、应用程序或二者皆有关联之名。比方说,假设你所在的公司叫做Effective Widgets,那么就可以在所有应用程序都会用到的那部分代码中使用EWS作前缀,如果有些代码只用于名为Effective Browser的浏览器项目中,那就在这部分代码中使用EWB作前缀。即便加了前缀,也难保不出现命名冲突,但是其几率会小很多。
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有"两字母前缀"(two-letter prefix)的权利,所以你自己选用的前缀应该是三个字母的。举个例子,假如开发者不遵循这条守则,使用TW这两个字母作前缀,那么就会出问题。iOS5.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)处理回调事宜:
//EOCSoundPlayer.h
#import <Foundation/Foundation.h>
@class EOCSoundPlayer;
@protocol EOCSoundPlayerDelegate <NSObject>
- (void)soundPlayerDidFinish:(EOCSoundPlayer *)player;
@end
@Interface EOCSoundPlayer : NSObject
@property (nonatomic, weak) id <EOCSoundPlayerDelegate> delegate;
- (id)initWithURL:(NSURL *)url;
- (void)playSound;
@end
//EOCSoundPlayer.m
#import "EOCSoundPlayer.h"
#import <AudioToolbox/AudioToolbox.h>
void completion (SystemSoundID ssID, void*clientData) {
EOCSoundPlayer *player = (__bridge EOCSoundPlayer *)clientData;
if ([player.delegate respondsToSelector:@selector(soundPlayerDidFinish:)]) {
[player.delegate soundPlayerDidFinish:player];
}
}
@implementation EOCSoundPlayer {
SystemSoundID _systemSoundID;
}
- (id)initWithURL:(NSURL *)url {
if ((self = [super init])) {
AudioServicesCreateSystemSoundID((__bridge CFURLRef)url, &_systemSoundID);
}
return self;
}
- (void)dealloc {
AudioServicesDisposeSystemSound(_systemSoundID);
}
- (void)playSound{
AudioServicesAddSystemSoundCompletion {
_systemSoundID,
NULL,
NULL,
completion,
(__bridge void *)self;
AudioServicesPlaySystemSound(_systemSoundID);
}
@end
}
这段代码看上去完全正常,不过你再看看该类目标文件中的符号表(symbol table),就会发现问题了:
屏幕快照 2017-04-13 09.32.46.png符号表中间有个名叫_completion的符号,这就是为了处理声音播放完毕之后的逻辑而创建的那个completion函数。虽说此函数是在实现文件里定义的,并没有声明于头文件中,不过它仍然算作"顶级符号"。这样的话,若在别处又创建了一个名叫completion的函数,则会于链接时发生类似下面这种"重复符号错误":
duplicate symbol _completion in:
build/EOCSoundPlayer.o
build/EOCAnotherClass.o
如果将代码发布为程序库,供他人在开发应用程序时使用,那么就更糟糕了。这等于办了件坏事: 因为已经有了名叫_completion的符号,所以使用此程序库的开发者就无法再创建名为completion的函数了。
由此可见,我们总是应该给这种C函数的名字加上前缀。比方说,在刚才那个例子中,播放完声音之后所执行的处理程序可以改名为EOCSoundPlayerCompletion。这么做还有个好处:若此符号出现在栈回溯信息中,则很容易就能判明问题源自哪块代码。
如果用第三方库编写自己的代码,并准备将其再发布为程序库供他人开发应用程序所用,那么尤其要注意重复符号问题。你的程序库所包含的那个第三方库也许还会为应用程序本身所引入,若是如此,那就很容易出现重复符号错误了。这是应该给你所用的那一份第三方库代码都加上你自己的前缀。例如,你准备发布的程序库叫做EOCLibrary,其中引入了名为XYZLibrary的第三方库,那么就应该把XYZLibrary中的所有名字都冠以EOC。于是,应用程序就可以随意使用它自己直接引入的那个XYZLibrary库了,而不必担心与EOCLibrary里的这个XYZLibrary相冲突。
虽说逐个改名是很令人厌烦的事情,不过若想避免命名冲突,还是得费这番功夫才行。读者也许会问: 为什么非要这么做呢?应用程序自己不要直接引入XYZLibrary,改用EOCLibrary里面的那个不就行了吗?没错,可以这么做,但是,应用程序也许还会引入另一个名为ABCLibrary的第三方库,而该库中又包含了XYZLibrary。此时,如果你和ABCLibrary库的作者都不给各自所用的XYZLibrary加前缀,那么应用程序依然会出现重复符号错误。还有一种可能就是,你的库里所用的XYZLibrary是X版本的,而应用程序却需要使用Y版本的某些功能,所以它必须自己再引入一份。你可以花些时间,使用几个流行的第三方库来开发一下iOS程序,那时会经常看到这种前缀的。
要点
- 选择与你的公司、应用程序或二者皆有关联之名称作类名的前缀,并在所有代码中均使用这一前缀。
- 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
网友评论