开发一个Cocoa framework,插件或者其他具有公开API的可执行文件时,需要采取与那些应用开发不同的方法和约定。你产品的初始客户端是开发者,非常重要的一点就是不要让他们为你的编程接口感到困惑。以下便是API的命名约定,能够帮助你让你的接口保持一致和清晰,这些对于你来说,迟早排得上用场。同样还有比较特殊的针对更重要的与框架有关的编程技术,例如,版本标注、二进制兼容性、错误处理和内存管理。此篇文章同样包含Cocoa命名约定和推荐的框架编程实践。
本文主要有两部分。编程接口的命名规范以及关于框架编程方面的讨论。
对于面向对象软件库的设计,经常忽视的一点是类、方法、函数、常量以及其它编程接口元素的命名。本部分将会讨论对于大多数的Cocoa接口的一些通用的命名约定。
尽可能简单明了,但是过于简单会让人看不懂:
代码 | 评注 |
---|---|
insetObject:atIndex | 不错 |
insert:at: | 不明了; 插入了什么? “at”表示什么意思? |
removeObjectAtIndex: | 不错 |
removeObject: | 不错,因为这个方法表明了移除这个参数对象 |
remove: | 不明了; 移除什么? |
一般来说,不要使用缩写。使用全拼,即使单词比较长
代码 | 评注 |
---|---|
destinationSelection | 不错 |
destSel | 不明了 |
setBackgroundColor: | 不错 |
setBkgdColor: | 不明了 |
你可能认为有些缩写必要有名,其实非你所料,尤其是当你的方法或者函数名遇到一个不同文化和语言的开发者时。比如在国内BAT,大家都知道说的是百度、阿里、腾讯,但是外国人却不知道说的是什么。
然而,少数缩写词确实常见,而且有很长的使用历史。你可以继续使用它们;可以参见后文的可接受的缩略词和首字母缩略词。
避免模棱两可的API命名,例如具有多种解释的方法名。
代码 | 评注 |
---|---|
sendPort | 是要发送还是需要返回端口? |
displayName | 是需要显示一个名字还是需要返回一个UI的标题? |
在整个Cocoa编程接口中的命名保持一致性。如果你不太确定,请查看当前的头文件或者引用文档。
当你一个类的方法需要利用到多态,这种情况下一致显得尤为重要。方法需要在不同的类中具有相同的作用,并且需要有相同的名字。
代码 | 评注 |
---|---|
- (NSInteger)tag | 在UIView, UIControl中定义 |
- (void)setStringValue:(NSString *) | 在许多Cocoa类中定义 |
指不能在当前命名中附带本身的属性,比如一个人叫张三,我们不要把他叫做张三人、人张三。
名字不能自引用
代码 | 评注 |
---|---|
NSString | 没问题 |
NSStringObject | 自引用 |
代码 | 评注 |
---|---|
NSUnderlineByWordMask | 没问题 |
NSTableViewColumnDidMoveNotification | 没问题 |
在编程接口命名中,前缀是一个重要部分。这个是用来区分软件不同的功能范围。这样的不同的范围,通常是一个框架中的包或者(例如基础框架和应用包)相关的框架。前缀能够防止第三方开发者定义的内容与Apple定义的内容相冲突。
前缀有规定的格式。它包含2-3个大写字母,并且不会使用下划线分割或者“子前缀”。下面有些例子:
前缀 | Cocoa框架 |
---|---|
NS | Foundation |
UI | UIKit |
AB | Address Book |
IB | Interface Builder |
在类、协议、函数、常量和定义结构体命名的时候,请使用前缀。不要在方法命名的时候使用前缀;方法是有通过类来定义的命名空间。同样,不要在结构体内的字段上使用前缀。
在API元素命名的时候,请遵循一些简单的排版约定:
对于由多个单词组成的命名,不要使用标点符号作为名字的一部分,或者作为分隔符(下划线、破折号等);而且每个单词的首字母要大写,并且连在一起(例如,runTheWordsTogether)——这个被称为驼峰式命名法。然而,要注意以下限制:
- 对于方法名称,首字母小写,并将其他单词的首字母大写。不要使用前缀。例如:
fileExistsAtPath:isDirectory:
。对于方法的命名有一个例外的地方就是以一个有名的首字母缩写词打头,例如NSImage的TIFFRepresentation方法。- 对于函数和常量的命名,对于相关的类使用相同的前缀,并且紧接着后面的一个单词首字母大写。例如:
NSRunAlertPanel
、NSCellDisabled
。- 避免使用下划线字符作为私有方法的命名前缀(对于使用下划线字符作为实例变量的命名前缀是允许的)。Apple保留这种约定的使用权。如果第三方使用,可能会导致命名空间的冲突;可能会在毫不知情的情况下覆盖了其中一个已经存在的私有方法,从而带来灾难性的后果。参见私有方法部分来获取对于私有API方法命名的约定建议。
类的命名中应当包含一个能够清楚的表明这个类(或者类的对象)是什么,或者是做什么的名词。这个名字需要有一个适当的前缀。Foundation框架和应用框架里面到处都是例子;例如: NSString, NSDate, NSScanner, NSApplication, UIApplication, NSButton, 以及UIButton。
协议应该依据组作用来命名:
一般情况下,大多数协议组相关的方法与任何类都不相关。这种类型的协议应该在命名上与类区分开来。一个常见的约定就是使用动名词(“…ing”)格式。例如:
代码 | 评注 |
---|---|
NSLocking | 不错 |
NSLock | 不是很好 (看起来像一个类的命名) |
一些协议中包含多个不相关的方法(而不是创建多个单独的小的协议)。这些协议与类相关,这是协议最重要的表达方式。在这种情况下,我们约定,保持协议和类名相同的方式。
这种协议的例子就是NSObject协议。可以使用这个协议中的方法来查询任意类层级的对象的位置,可以调用指定方法,增加或减少它的引用数。因为类NSObject提供了这些方法的初始表达式,协议的命名在类名之后。
你如何命名头文件非常重要,因为约定中,你用它来显示这个文件包含的内容:
声明一个单独的类或协议。如果一个类或协议不属于一个组的一部分,将它的声明放在一个单独的文件中,这个文件的名字是已经声明过的类或者协议。
头文件 | 声明 |
---|---|
NSLocale.h | NSLocale类 |
声明相关的类和协议。对于一个含有相关声明的组(类、分类和协议),将声明放在一个与初始类、分类或协议命名相关的文件中。
头文件 | 声明 |
---|---|
NSString.h | NSString和NSMutableString类 |
NSLock.h | NSLocking协议和NSLock, NSConditionLock, 以及 NSRecursiveLock 类 |
包含框架头文件。每一个框架应当有一个头文件,在框架后面命名,这个头文件应当包含所有的框架的公共头文件。
头文件 | 声明 |
---|---|
Foundation.h | Foundation.framework |
在其它框架中添加API到一个类中。如果你想要在一个框架中中声明一个在其它框架中类的分类中的方法,请在原始类的命名后面追加“Additions”;一个典型的例子就是应用包中的NSBundleAdditions.h 头文件。
相关的函数和数据类型。如果你有一组相关的函数、常量、结构体以及其它数据类型,可以将他们放在一个恰当命名的头文件中,例如 NSGraphics.h(应用包)。
方法或许是你编程接口中最常见的元素,因此在它们的命名方面应当特别小心。
方法名应该以小写字母打头,然后紧接着后面每一个单词的首字母大写。不要使用前缀。可以参见前面的排满约定。
对于以上指南,有两个特别的例外,你可能用一个很有名的大写缩写词为方法名打头(例如 TIFF或PDF),而且你可能会使用前缀来标记组以及私有方法(参见上面的私有方法)。
当一个方法表示一个对象执行的动作的话,请以一个动词打头:
- (void)invokeWithTarget:(id)target;
- (void)selectTabViewItem:(NSTabViewItem *)tabViewItem
不要使用“do”或者“does”作为方法名的一部分,因为这些辅助的动词很少能够增强意思。同样,绝不要在动词前面使用副词或者形容词。
如果一个方法返回给调用者一个属性的话,用这个属性命名方法。没有必要使用“get”。除非不是直接返回一个或多个返回值。
方法命名 | 评述 |
---|---|
- (NSSize)cellSize | 正确 |
- (NSSize)calcCellSize | 错误 |
- (NSSize)getCellSize | 错误 |
在所有参数前面使用关键词
方法命名 | 评述 |
---|---|
- (void)sendAction:(SEL)aSelector toObject:(id)anObject forAllCells:(BOOL)flag | 正确 |
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag | 错误 |
在参数之前描述这个参数
方法命名 | 评述 |
---|---|
- (id)viewWithTag:(NSInteger)aTag | 正确 |
- (id)taggedView:(int)aTag | 错误 |
当你想要创建一个比继承的方法更加明确的方法的时候,请在方法结束之后,添加一些关键词。
方法命名 | 评述 |
---|---|
- (id)initWithFrame:(CGRect)frameRect | NSView,UIView |
- (id)initWithFrame:(NSRect)frameRect mode:(int)aMode cellClass:(Class)factoryId numberOfRows:(int)rowsHigh numberOfColumns:(int)colsWide | NSMatrix, NSView的子类 |
不要在描述属性的关键词上使用“and”。
方法命名 | 评述 |
---|---|
- (int)runModalForDirectory:(NSString )path file:(NSString ) name types:(NSArray *)fileTypes | 正确 |
- (int)runModalForDirectory:(NSString )path andFile:(NSString )name andTypes:(NSArray *)fileTypes | 错误 |
尽管在这个例子中,采用“and”看起来还可以,但是当你创建越来越多关键词方法的时候,会出问题。
如果一个方法描述了两个单独的动作,可以使用“and”来连接他们。
方法命名 | 评述 |
---|---|
- (BOOL)openFile:(NSString )fullPath withApplication:(NSString )appName andDeactivate:(BOOL)flag | NSWorkspace |
存取方法是用来设置和返回一个对象的属性值。有推荐的格式,取决于这个属性表达方式:
如果这个属性是名词的话,格式是这样的:
- (type)noun;
- (void)setNoun:(type)aNoun;
举个例子:
- (NSString *)title;
- (void)setTitle:(NSString *)aTitle;
如果这个属性是形容词,格式是这样的:
- (BOOL)isAdjective;
- (void)setIsAdjective:(BOOL)flag;
举个例子:
- (BOOL)isEditable;
- (void)setEditable:(BOOL)flag;
如果这个属性是一个动词,格式是这样的:
- (BOOL)verbObject;
- (void)setVerbObject:(BOOL)flag;
举个例子:
- (BOOL)showsAlpha;
- (void)setShowsAlpha:(BOOL)flag;
动词应当使用简单的现在时。
不要通过分词形式将动词转换成形容词:
方法命名 | 评述 |
---|---|
- (void)setAcceptsGlyphInfo:(BOOL)flag | 正确 |
- (BOOL)acceptsGlyphInfo | 正确 |
- (void)setGlyphInfoAccepted:(BOOL)flag | 错误 |
- (BOOL)glyphInfoAccepted | 错误 |
你可能会使用情态动词来阐述意思,但是不用使用“do” 或“does”。
方法命名 | 评述 |
---|---|
- (void)setCanHide:(BOOL)flag | 正确 |
- (BOOL)canHide | 正确 |
- (void)setShouldCloseDocument:(BOOL)flag | 正确 |
- (BOOL)shouldCloseDocument | 正确 |
- (void)setDoesAcceptGlyphInfo:(BOOL)flag | 错误 |
- (BOOL)doesAcceptGlyphInfo | 错误 |
仅当方法间接返回对象和值的时候,使用“get”命名。而且仅当多个条目需要返回的时候。
方法命名 | 评述 |
---|---|
- (void)getLineDash:(float *)pattern count:(int *)count phase:(float *)phase | NSBezierPath |
在以上这些方法中,方法的实现应当考虑针对这些输入输出参数可接受NULL值,来指明调用者不必对一个或者多个返回值感兴趣。
代理方法是指在某一事件发生的时候,一个对象调用它的代理(如果实现了这个代理)。它们有独特的格式,同样适用于对象数据源的方法调用。
方法打头请标明是哪个类的对象发送信息的:
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;
类名省略前缀,并且第一个字母小写。
一个冒号后附带类名(参数是代理对象的实例),除非这个方法只有一个参数,发送者。
- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender;
有一个例外就是,方法作为通知被发送的结果来调用。这种情况下,这个单独的参数就是通知对象。
- (void)windowDidChangeScreen:(NSNotification *)notification;
在方法名上使用“did” 或 “will”用来通知代理,有事情已经发生或者即将发生。
- (void)browserDidScroll:(NSBrowser *)sender;
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;
尽管你可以在方法名上使用“did” 或“will”,在方法被调用的时候,请求代理为另外一个对象做些事情,还是推荐“should”。
- (BOOL)windowShouldClose:(id)sender;
对于那些管理对象集合的对象(每一个被称为集合的元素),约定为以下方法格式:
- (void)addElement:(elementType)anObj;
- (void)removeElement:(elementType)anObj;
- (NSArray *)elements;
例如:
- (void)addLayoutManager:(NSLayoutManager *)obj;
- (void)removeLayoutManager:(NSLayoutManager *)obj;
- (NSArray *)layoutManagers;
以下是对这个指南的限制和改进:
- (void)insertLayoutManager:(NSLayoutManager *)obj atIndex:(int)index;
- (void)removeLayoutManagerAtIndex:(int)index;
还有一些针对于MRC的说明,这里就不列举了。在非ARC下,确定好需要强弱引用。如果是强引用,使用NSArray、NSDictionary, NSSet等;如果是弱引用,则使用NSPointerArray、NSMapTable、NSHashTable。
这里有一些设计方法参数命名的一般规则:
一般来说(在Cocoa中),以下关键词和参数会同时使用:
...action:(SEL)aSelector
...alignment:(int)mode
...atIndex:(int)index
...content:(NSRect)aRect
...doubleValue:(double)aDouble
...floatValue:(float)aFloat
...font:(NSFont *)fontObj
...frame:(NSRect)frameRect
...intValue:(int)anInt
...length:(int)numBytes
...point:(NSPoint)aPoint
...stringValue:(NSString *)aString
...tag:(int)anInt
...target:(id)anObject
...title:(NSString *)aString
在大多数情况下,私有方法名称通常遵循与公共方法名称相同的规则。然而,一个常见的约定就是给私有方法一个前缀,以便它很容易与公共方法进行区分。即使采用这种约定,私有方法的名称也可能导致一种特殊类型的问题。当您设计Cocoa框架类的子类时,您无法知道您的私有方法是否无意中覆盖了具有相同名称的私有框架方法。
大多数Cocoa框架中的私有方法有一个下划线前缀(例如:_fooData ),用来标记它是私有方法。基于这个事实,有以下两个建议:
NSView
或UIView
)的子类,你想要绝对确定你的私有方法的名称与超类中的名称不同,你可以在你的私有方法前面加上一个你自己的前缀。这个前缀尽可能的唯一,或许基于你的公司或者项目,诸如XX_
格式。所以,若你的项目叫做Byte Flogger,那么这个方法名可以是BF_addObject: 。尽管给私有方法加上前缀的命名方式看起来像是与先前的提出的方法以类为区分的命名空间存在矛盾之处,但是这里的意图在于:防止无意识的重写了父类的私有方法。
Objective-C 允许你通过函数来实现和方法一样的功能。当潜在的对象总是一个单例或者当你要处理明显的功能性的子系统,你可以使用函数,而不是类中的方法。
函数有一些通用的命名规则,您应该遵循:
函数的命名格式有点类似方法的命名,但是有一些例外:
大多数的函数名以动词打头,用来表示这个函数的作用:
NSHighlightRect
NSDeallocateObject
如果这个函数是用来查询属性的话,还有一套其它的规则:
unsigned int NSEventMaskFromType(NSEventType type)
float NSHeight(NSRect aRect)
const char *NSGetSizeAndAlignment(const char *typePtr, unsigned int *sizep, unsigned int *alignp)
BOOL NSDecimalIsNotANumber(const NSDecimal *decimal)
这个部分介绍了声明的属性,实例变量,常量,通知和异常的命名约定。
属性的声明实际上是声明属性的读写方法,所以声明属性的命名约定大体上和存取方法的命名约定相同(参见存取方法)。
如果属性是一个名词或者动词,格式是:
@property (…) type nounOrVerb;
例如:
@property (strong) NSString *title;
@property (assign) BOOL showsAlpha;
如果声明属性是一个形容词,然而需要注意的是,属性名忽略掉“is”前缀,但是指定约定的get读取方法名,例如:
@property (assign, getter=isEditable) BOOL editable;
大多数情况下,当你使用一个声明的属性的时候,你同样需要 synthesize 一个相对应的实例变量。
确保实例变量的名字简明的描述存储的属性。通常情况下,你不能直接访问实例变量;而是要通过读写方法来访问(你可以在init和dealloc方法中直接访问实例变量)。为了更加显著,可以采用下划线来命名实例变量,例如:
@implementation MyClass {
BOOL _showsTitle;
}
如果你通过使用一个声明属性来synthesize一个实例变量的话,在@synthesize语句中指定实例变量的名字。
@implementation MyClass
@synthesize showsTitle=_showsTitle;
@end
当向类中添加实例变量的时候,需要考虑以下几点:
避免直接声明公共的实例变量
开发者应当关注的是对象的接口,而不是如何存储数据的细节。你可以通过使用声明属性以及synthesize相关的实例变量。
如果你需要声明一个实例变量,避免用@private
或者@protected
来声明。
如果您希望您的类被子类化,并且这些子类需要直接访问数据,请使用
@protected
。
如果一个实例变量是一个类实例的可访问属性,确保你给他写了读写方法(如果可能,请使用声明属性)。
常量的命名规则根据常量的创建方式而有所不同。
typedef enum _NSMatrixMode {
NSRadioModeMatrix = 0,
NSHighlightModeMatrix = 1,
NSListModeMatrix = 2,
NSTrackModeMatrix = 3
} NSMatrixMode;
注意类型定义的标签(例如上面的_NSMatrixMode)是不需要的。enum {
NSBorderlessWindowMask = 0,
NSTitledWindowMask = 1 << 0,
NSClosableWindowMask = 1 << 1,
NSMiniaturizableWindowMask = 1 << 2,
NSResizableWindowMask = 1 << 3
};
使用const为浮点值创建常量。如果常量与其他常量无关,则可以使用const创建整数常量;否则,请使用枚举。
下面是一个const常量的命名格式的例子:
const float NSLightGray;
如同常量枚举一样,对于函数的命名约定也是一样的(参见函数命名)。
通常,不要使用#define
预处理程序命令来创建常量。对于整数常量,使用枚举;对于浮点常量,使用const限定符,如上所述。
可以用大写符号做标记,这样预处理器就可以判断代码是否需要被处理。例如:
#ifdef DEBUG
注意,由编译器定义的宏在头部和尾部有双下划线。例如:
__MACH__
对于通知名和字典键,我们可以定义字符串常量。通过字符串常量,你可以确保编译器检验指定的正确的值(是指执行拼写检查)Cocoa框架提供许多字符串常量的例子,例如:
APPKIT_EXTERN NSString *NSPrintCopies;
实际上,NSString的值是赋值给一个实现文件中的常量的。(请注意,在Objective-C中,APPKIT_EXTERN宏是全局变量。)
通知和异常的命名约定遵守同样的规则。但是又有他们自己推荐的使用规则。
如果一个类有一个代理,大多数的通知有可能通过代理中定义的代理方法来接收的。这些通知的名称应反映相应的委托方法。例如,一个全局的 NSApplication
代理对象自动注册接收一个applicationDidBecomeActive:
的消息,当application任何时候发送一个NSApplicationDidBecomeActiveNotification
。
通知是通过全局的NSString对象来标识的,以以下方式来组合名字:
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
例如:
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification
NSColorPanelColorDidChangeNotification
尽管你可以选择任意意图来使用异常(就是说NSException类和相关函数提供的机制)但是Cocoa保留一些编程错误的异常,例如:数组索引越界异常。Cocoa不使用异常来处理常规的预期错误情况。对于这些情况,请使用返回值,例如nil,NULL,NO或错误代码的返回值。更多细节,请参见Error Handling Programming Guide。
异常是通过全局的NSString对象来标识的,以以下方式来组合名字:
[Prefix] + [UniquePartOfName] + Exception
名称的唯一部分应该将组成单词组合在一起并将每个单词的首字母大写。以下是一些例子:
NSColorListIOException
NSColorListNotEditableException
NSDraggingException
NSFontUnavailableException
NSIllegalSelectorException
一般来说,你在设计你的编程接口的时候,不应当缩写名字(参见一般原则)。然而,以下列举的缩写词已经创建并且在过去使用了很久,所以你可以继续使用他。这里有额外的两件事情需要注意:
imageRep
, col
(column
的简写), obj
, 和 otherWin
)。缩写词 | 意思和评注 |
---|---|
alloc | Allocate 分配 |
alt | Alternate 轮流的 |
app | Application 应用 例如,全局的应用对象NSApp。然而在代理方法、通知等地方,还是用全拼“application”。 |
calc | Calculate 计算 |
dealloc | Deallocate 取消分配 |
func | Function 函数 |
horiz | Horizontal 水平 |
info | Information 信息 |
init | Initialize 初始化 用户初始化对象方法中 |
max | Maximum 最大 |
min | Minimum 最小 |
msg | Message 消息 |
nib | Interface Builder archive 界面生成器归档文件 |
pboard | Pasteboard 剪切板(仅在常量中使用) |
rect | Rectangle 矩形 |
Rep | Representation 表现(在类名中使用,例如NSBitmapImageRep)。 |
temp | Temporary 临时的 |
vert | Vertical 垂直的 |
你可以使用已经在计算机工业领域非常常见的缩写词和首字母大写词。以下是一些比较有名的缩写词:
ASCII
PDF
XML
HTML
URL
RTF
HTTP
TIFF
JPG
PNG
GIF
LZW
ROM
RGB
CMYK
MIDI
FTP
框架开发人员在编写代码时必须比其他开发人员更加谨慎。很多的客户端应用都会链接到他们的框架中,正是因为这样广泛的暴露,任何框架的缺陷,都会放大到整个系统。下面的内容讨论一些你可以采纳的编程技巧,用来确保你的框架的效率和完整性。
以下是包含了框架初始化的意见和建议。
+initialize
方法为您提供了一个位置,可以在调用类的任何其他方法之前懒惰地执行一次代码。通常被用来设置类的版本号(参见版本和兼容性)。
运行时将+initialize
方法发送到继承链中的每个类,即使它尚未实现它。因此它可能不止一次地调用类的+initialize
方法(例如,如果子类没有实现它)。通常,您希望初始化代码只执行一次。确保这种情况发生的一种方法是使用dispatch_once()
:
+ (void)initialize {
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
// the initializing code
}
}
if (self == [NSFoo class]) {
// the initializing code
}
绝不要显示的调用initialize方法。如果你想要触发初始化,调用一些无害的方法,例如:
[NSImage self];
指定的初始化函数是调用父类的init
方法的类的init
方法。(其它初始化器调用类定义的初始化方法)。每个公共类都应该有一个或多个指定的初始化函数。指定初始化方法的例子有:NSView
类的initWithFrame:
和NSResponder
的init
方法。init
方法并不意味着需要重写,比如NSString
类和其它类簇中的抽象类,子类应该来自己实现。
指定的初始化函数应当明确的指定,因为这对于想要依据你的类创建子类来说非常重要。子类仅重写指定的初始化函数。
当你要实现一个框架的类时,你经常需要也实现它的存档方法:initWithCoder:
和 encodeWithCoder:
。注意不要当在对象解归档的时候,在初始化的代码中执行不会发生的事情。实现这一目标的一个好方法是,如果您的类实现了归档,则从您指定的初始化程序和initWithCoder调用一个公共例程:(这是一个指定的初始化函数)。
一个设计比较好的初始化方法应当通过完成以下几部来确保正确的检测和错误输出:
//初始化过程中的错误检测
- (id)init {
self = [super init]; // Call a designated initializer here.
if (self != nil) {
// Initialize object ...
if (someError) {
self = nil;
}
}
return self;
}
向框架添加新类或方法时,通常不必为每个新功能组指定新版本号。开发人员通常执行(或应该执行)Objective-C运行时检查,例如respondsToSelector:以确定某个功能是否在给定系统上可用。这些运行时测试是检查新功能的首选和最动态的方法。
但是,您可以使用多种技术来确保正确标记每个新版本的框架,并使其与早期版本尽可能兼容。
当有一个新增特性或者bug被修复时,通过运行时测试是不容易检测出来的,你应当以某种方式告知开发者来检测这种变化。一种方式就是以归档的形式来存储框架的确切版本号,同时需要让开发者可见这些内容:
如果你的框架对象需要写入nib文件,他们必须能够自归档。你同样需要通过使用归档机制存储文档数据来归档任何文档。
你应当考虑以下关于归档方面的问题:
大多数Cocoa框架方法都不会强制开发人员捕获和处理异常。这是因为异常不是作为执行的正常部分引发的,并且通常不用于传达预期的运行时或用户错误。这些错误的示例包括:
然而,Cocoa对于以下情况会产生异常来指明程序或者逻辑错误:
期望开发人员在测试期间捕获这些错误并在发布应用程序之前解决它们;因此,应用程序不需要在运行时处理异常。如果一个异常往外扩散,应用没有捕获它,高级别的默认处理器通常会处理它,并且会报告异常,然后让它们继续执行。开发人员可以选择将此默认异常捕获器替换为提供更多错误详细信息的异常捕获器,并且提供一个可选项来保存数据并且退出应用程序。
错误是Cocoa框架与其他软件库不同的另一个领域。Cocoa方法通常不返回错误代码。在存在一个合理或可能的错误原因的情况下,这些方法依赖于对布尔或对象(nil / non-nil)返回值的简单测试;记录NO或零返回值的原因。您不应该使用错误代码来指示要在运行时处理的编程错误,而是引发异常,或者在某些情况下只记录错误而不引发异常。
例如,NSDictionary
的objectForKey:
方法要么返回找到的对象,要么返回nil
,如果它找不到对象。NSArray
的objectAtIndex:
方法不会返回nil
(除非重写一般语言约定,将任何信息转换成nil,导致返回nil),因为NSArray对象不能存储nil值,并且根据定义,任何越界访问都是一个编程错误,应该导致异常。许多初始化方法会因为通过提供的参数不能够初始化,从而导致返回nil。
在少数情况下,一个方法有一个返回多个不同的错误代码,应当用引用参数来指定它,返回一个错误的代码,一个本地话的错误字符串,或者其他的描述错误的信息。例如,你肯能需要返回一个NSError对象来表示错误;可以查看框架中的NSError.h头文件来获取细节。这个参数一般来说是一个直接返回的BOOL或者nil。该方法还应遵守以下约定:所有引用参数都是可选的,因此如果发件人不希望了解错误,则允许发送者为错误代码参数传递NULL。
如何处理框架数据会对性能,跨平台兼容性和其他目的产生影响。这部分讨论涉及的框架数据的技巧。
出于性能原因,最好将尽可能多的框架数据标记为常量,因为这样做会减小Mach-O二进制文件的__DATA
段的大小。非const的全局和静态数据最终会出现在__DATA section
的__DATA
段中。这种数据占用了使用框架的应用程序的每个运行实例中的内存。虽然额外的500字节(举个例子)可能看起来不那么糟糕,但它可能会导致所需页数增加 - 每个应用程序额外增加4千字节。
您应该将任何常量数据标记为const。如果block中没有char *
指针,会导致数据处在__TEXT
段中(这里会使之成为真正的常量);否则的话,它处于__DATA
段,但是却不能写入(除非预绑定没有完成或者通过在加载时二进制的偏移来改变它)。
您应该初始化静态变量以确保它们合并到__DATA
段的__data
部分而不是__bss
部分。如果没有明显的值用于初始化,请使用0,NULL,0.0或任何适当的值。
针对位段使用有符号的值,特别是一位的位段,这样会导致如果代码将这个值作为boolean
值,会出现未定义行为。一位位域应始终为无符号。因为可以存储在这样的位域中的唯一值是0和-1(取决于编译器实现),所以将该位域与1进行比较是错误的。例如,如果您在代码中遇到类似这样的内容:
BOOL isAttachment:1;
int startTracking:1;
您应该将类型更改为unsigned int
。
另外一个和位段相关的内容是归档。一般来说,你不能以位段本身的格式来写入到磁盘或者归档中,因为当在另外一个架构或者其它编译器上读取的时候,格式可能不同。
在框架代码中,避免全部内存分配是最好的课程。如果由于某种原因需要临时缓冲区,通常使用栈比分配缓冲区更好。但是,堆栈的大小有限(通常总共512KB),因此使用堆栈的决定取决于您需要的函数和缓冲区的大小。通常,如果buffer大小是1000bytes(或者MAXPATHLEN)或者更小,可以使用栈。
一个改进是开始使用栈,但如果大小要求超出栈缓冲区大小,则切换到堆缓冲区中。以下有例子:
#define STACKBUFSIZE (1000 / sizeof(YourElementType))
YourElementType stackBuffer[STACKBUFSIZE];
YourElementType *buf = stackBuffer;
int capacity = STACKBUFSIZE; // In terms of YourElementType
int numElements = 0; // In terms of YourElementType
while (1) {
if (numElements > capacity) { // Need more room
int newCapacity = capacity * 2; // Or whatever your growth algorithm is
if (buf == stackBuffer) { // Previously using stack; switch to allocated memory
buf = malloc(newCapacity * sizeof(YourElementType));
memmove(buf, stackBuffer, capacity * sizeof(YourElementType));
} else { // Was already using malloc; simply realloc
buf = realloc(buf, newCapacity * sizeof(YourElementType));
}
capacity = newCapacity;
}
// ... use buf; increment numElements ...
}
// ...
if (buf != stackBuffer) free(buf);
你应当意识到泛型的对象比较方法isEqual:
和对象相关的比较方法,例如isEqualToString:
方法之间的重要区别。isEqual:
方法允许你传入任意对象作为参数,并且如果对象不是同一个类会返回NO
。诸如isEqualToString:
和isEqualToArray:
方法通常假设参数是指定的类型(也就是接收者的类型)。因此,它们不执行类型检查,因此它们更快但不安全。对于从外部源检索的值,例如应用程序的信息属性列表(Info.plist
)或首选项,使用isEqual:
是首选,因为它更安全;当类型已知时,请使用isEqualToString:
代替。
和isEqual:方法相关的一点就是它和hash方法有关。对象一个最基本的不变的地方就是被放入一个基于哈希的例如NSDictionary
或NSSetCocoa
集合中,如果[A isEqual:B] == YES
,那么[A hash] == [B hash]
。如果你重写你的类的isEqual:
方法,你应当也要重写hash方法来维持这一个不变的条件。默认的isEqual:
方法寻找和每一个对象地址相等的指针,并且hash方法返回的hash值是基于每一个对象的地址,所以还是保持了这个不变性。