cocoa编码指南(翻译)
开发cocoa框架、插件或者其它带公共API的可执行文件需要不同于应用开发一些方法和惯例。你的产品的主要客户是开发人员,这些人员不会对你的接口迷惑是很重要的。这时候,API的命名规则就将派上用场,它可以帮助你让你的接口清晰、一致。有些编程技术对framework来说是特殊或者极其重要的,比如版本、兼容性、错误处理和内存管理。这个话题包含了Cocoa的命名规则和framework的编程练习建议。
文档结构
本文的主题包含两种基本类型。第一种也是最大的一组是编程接口的命名规则。这是与Apple应用再它自己的Cocoa框架上相同的规则。(除了少数例外)这些命名规则的文章包含一下内容:
- 代码命名基础
- 方法命名
- 函数命名
- 属性命名和数据类型
- 可接受的缩写和缩略词
第二种讨论framework编程的方法(目前成员之一):
- 给framework开发者的一些技巧与技术
代码命名基础
面向对象的软件库的设计一个经常被忽视的方面是类、方法、函数、常量和编程接口的其他元素的命名。这个部分讨论一些对多数Cocoa接口通用的命名规则。
一般原则
清晰
- 尽可能清晰、简洁,但不能因为简短而牺牲清晰的特性:
代码 | 评价 |
---|---|
insertObject:atIndex: | Good |
insert:at: | 不清晰,插入的是什么?at表示什么 |
removeObjectAtIndex: | Good |
removeObject: | Good,它移除参数中提到的对象 |
remove: | 不清晰,什么将要被移除? |
- 一般来说,不要使用缩写名称。拼写完全,即时他们很长
代码 | 评价 |
---|---|
destinationSelection | Good |
destSel | 不清晰 |
setBackgroundColor: | Good |
setBkgColor: | 不清晰 |
你可能认为某个缩写是大家都知道的,但是它可能不是。特别是遇到有不同文化或语言背景的开发者调用你的方法或者函数。
- 但是,某些缩写在已经在很长一段历史里被广泛使用了。那么你可以继续使用它们,你可以查看** 被接受的缩写和缩略词**
- 避免在API名字里使用有歧义的单词,比如可以以超过1种方式解读的方法名
代码 | 评价 |
---|---|
sendPort | 它表示发送一个接口还是返回? |
displayName | 它是显示一个名字还是在用户界面里返回接收者的标题? |
一致性
- 尽量在Cocoa编程接口里自始至终使用一致的命名。如果你不确定,浏览当前头文件或者先例的参考文档。
- 当你有一个使用了多态方法的类的时候,一致性是尤其重要的。在不同类做了相同事情的方法应该有相同的名字。
代码 | 评价 |
---|---|
- (NSInteger)tag | 在NSView,NSCell,NSControl中定义过 |
- (void)setStringValue:(NSString *) | 在很多Cocoa类里定义过 |
你也可以查看方法参数
无自身参照
- 名称不应该自我涉及
代码 | 评价 |
---|---|
NSString | Okay. |
NSStringObject | 自我涉及 |
- 对这个规则来说被隐藏的常量是个例外。(因此可以按位运算结合)例如通知名的常量。
代码 | 评价 |
---|---|
NSUnderlineByWordMask | Okay. |
NSTableViewCalumnDidMoveNotification | Okay |
前缀
在编程接口名中,前缀是非常重要的。它们可以区分软件的不通功能区域。通过这个软件被打包成一个framework或者与framework相近的东西。第三方开发者和苹果定义的前缀标识能够防止冲突。(当然也包括苹果的framework之间的标识)
- 前缀有一个规定的格式。它由2、3个大写字母组成,并且不使用下划线或者子前缀。这是几个例子
前缀 | Cococa Framework |
---|---|
NS | Foundation |
NS | Application Kit |
AB | Address Book |
IB | Interface Builder |
在为类、协议、函数、常量和typedef结构体中命名时使用前缀。不要在为方法命名时使用前缀;方法存在于定义他们的类的命名空间中。当然,也不要在文件命名时使用前缀。
书写规则
下面是一些命名API元素时的一些书写规则
- 对多个单词命名,不要使用标点符号作为命名的一部分或者分离它们(下划线,破折号等等);相反,利用单词的首字母组合起来(例如:runTheWordsTogether)-这就是著名的驼峰规则。然而,还是要注意以下条件:
- 对于方法的命名,以小写字母作为单词的开头。不要使用前缀。
fileExistsAtPath:isDirectory:
- 对这个指南有一个方法命名的另外就是以广为人知的首字母缩写,例如,TIFFRepresentation(NSImage)。
NSRunAlertPanel NSCellDisabled
- 避免使用下划线作为前缀意义在于它是被用于私有的方法命名(可以用它做实例变量).苹果保留其使用。第三方使用可能导致命名空间的冲突。它们可能无意间重写一个它们拥有的私有方法。参考** 私有方法 **的来查看私有API的书写规则建议。
类和协议命名
类名应该包含一个对类的清晰的表达或者要做什么的名词。命名应该有一个恰当的前缀(参考前缀)。Foundation和application框架里充满了例子;比如:NSString,NSDate,NSScanner,NSApplication,UIApplication,NSButton和UIButton。
协议(Protocols)应当根据它的分组行为来命名
- 大部分协议会把一些彼此相关但又不合类关联的方法归结在一起,形成一个特殊的方法集合。这种类型的协议命名应当不与其类名混淆。一个通用习惯是使用动名词("...ing")的格式:
代码 | 评价 |
---|---|
NSLocking | Good |
NSLock | Poor(像一个类的名字) |
- 有些协议组成一些并不关联的方法(而不是创建几个分离的小协议)。这些协议经常跟一个类联系起来,主要通过这个类来表达协议。在这种情况下,习惯上让协议的名字与类保持一致。
这种协议的一个例子是NSObject协议。你可以使用这种协议的方法来查询在一个对象在类里的层次,去调用一个指定的方法,增加或者减少它的引用计数。由于NSObject类提供了这些方法的主要表现,所以我们使用类来做协议的命名。
头文件
如何给头文件命名是非常重要的,因为通过合理的习惯,你用来指示这些文件包含的内容:
- 声明一个独立的类或协议。如果一个类或代理是独立的,请将其用它的类名或协议名命名的文件里独立声明。
头文件 | 声明 |
---|---|
NSLocale.h | NSLocale类 |
- 声明相关的类跟协议。把相关联的声明(类、类目和协议)放到使用与同个文件里,并使用主要的类或类目或协议做名称。
头文件 | 声明 |
---|---|
NSString.h | NSString和NSMutableString类 |
NSLock.h | NSLocking协议和NSLock,NSConditionLock和NSRecursiveLock类 |
- 包含framework的头文件。每个framework应该有一个以其命名的头文件,其中包含了所有这个framework的头文件。
头文件 | Framework |
---|---|
Foundation.h | Foundation.framework |
- 给其它的framework添加接口。如果你在某个framework里给别的framework
的类添加类别,添加给原始的类添加"Additions"作为头文件的名称;在Application Kit里的一个例子就是NSBundleAdditions.h头文件。 - 联系方法与数据类型。如果你有一个相关联的方法、常数、结构体和其他数据类型,把他们放到一个适当名字的头文件里,比如NSGraphics.h(Application Kit)。
方法命名
方法可能是你程序界面里最普遍的元素了,所以你应该尤其注意如何给它们命名。这个部分我们方法方面的命名。
一般规则
这里有一些需要在方法命名里常用的指南需要记住:
以小写字母开头并且后面单词的第一个字母要大写。不要使用前缀。参照书写规则。
对这个指南来说有两个特例。你可能使用某些常用的大写字母缩写(比如TIFF或者PDF),你可能使用来标识是有方法(详细查看私有方法)。对于一个对象动作的方法,使用动词来开头:
- (void)invokeWithTarget:(id)target;
- (void)selectTabViewItem:(NSTabViewItem *)tabViewItem;
不要使用"do"或"dose"作为命名的一部分,因为这些辅助动词很少有添加的意义。同时,也不要再动词前面使用副词跟形容词。
- 如果这个方法返回返回接收者的某个属性,直接使用属性命名。使用"get"是没有必要的,除非间接返回一个或多个值。
代码 | 评价 |
---|---|
- (NSSize)cellSize; | 正确 |
- (NSSize)calcCellSize; | 错误 |
- (NSSize)getCellSize; | 错误 |
你也可以参考"访问方法"
- 在所有参数前使用关键字。
代码 | 评价 |
---|---|
- (void)sendAction:(SEL)aSelector toObject:(id)anObject forAllCells:(BOOL)flag; | 正确 |
- (void)sendAction:(SEL)aSelector :(id)anObjject :(BOOL)flag; | 错误 |
- 在参数前使用词汇修饰参数。
代码 | 评价 |
---|---|
- (id)viewWithTag:(NSInteger)aTag; | 正确 |
- (id)taggedView:(int)aTag; | 错误 |
- 当你添加一个比继承的方法更加详细的时候,在现存方法之后添加新的关键字。
代码 | 评价 |
---|---|
- (id)initWithFrame:(CGRect)frameRect; | NSView,UIView |
- (id)initWithFrame:(CGRect)frameRect mode:(int)aMode cellClass:(Class)factoryId numberOfRows:(int)rowsHigh numberOfColumns:(int)colsWide; | NSMatrix,NSView的子类 |
虽然"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)setAdjective:(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; | 错误 |
- 你可能使用情态动词(使用"can","should","will"等在动词前面修饰)来阐明意思,但是不要使用"do"或"dose"。
代码 | 评价 |
---|---|
- (void)setCanHide:(BOOL)flag; | 正确 |
- (BOOL)canHide; | 正确 |
- (void)setShouldCloseDocument:(BOOL)flat; | 正确 |
- (BOOL)shouldCloseDocument; | 正确 |
- (void)setDoesAcceptGlyphInfo:(BOOL)flag; | 错误 |
- (BOOL)doesAcceptGlyphInfo; | 错误 |
只有在一个方法间接返回对象或值的时候时候才使用"get"。当一个方法需要返回多个参数的时候,应该使用一下格式:
代码 | 评价 |
---|---|
- (void)getLineDash:(float *)pattern count:(int *)count phase:(float *)phase; | 正确 |
在如下这些方法里,对于这些输入输出参数,实现里面应该可以接受NULL来表示调用者并不不要一个或多个返回值。
代理方法
代理方法是单一个事件发生的时候,某个对象在它的代理里调用的方法。他们有特有的格式,同样也是用于一个对象的数据源调用:
- 是用发送消息的类的对象为方法的起始:
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;
类名省略前缀并且首字母小写。
- 除非方法只有"调用者"一个参数,否者类名需要附加一个冒号。
- (BOOL)applicationOpenUntitleFile:(NSApplication *)sender;
- 这里有一个特例就是,当这个方法被左右通知发送的方法被调用的时候。这这种情况下,这个唯一的参数就是通知对象。
- (void)windowDidChangeScreen:(NSNotification *)notification;
- 虽然你可以使用"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;
以下是一些限制跟规定:
- 如果集合是无须的,返回一个NSSet对象而不是NSArray对象。
- 如果给集合插入的位置是很重要的,使用与下列方法类似的方法代替,或者添加到上面的方法里:
- (void)insertLayoutManager:(NSLayoutManager *)obj atIndex:(int)index;
- (void)removeLayoutManagerAtIndex:(int)index;
集合方法有很多实现细节需要记住:
- 这些方法通常需要持有插入对象的所有权,所以添加或插入他们的代码比如retain它们,并且移除他们的代码也必须release它们。
- 如果插入对象需要有 一个指向主对象的指针,你一般会使用一个set...方法来设置一个指针,但是不要retain。在
insertLayoutManager:atIndex:
方法里,** NSLayoutManager ** 类使用如下方法:
- (void)setTextStorage:(NSTextStorage *)textStorage;
- (NSTextStorage *)textStorage;
你通常不会直接调用setTextStorage:
方法,但是可能会想重写它。
上面集合方法的另一个通用例子来自NSWindow类:
- (void)addChildWindow:(NSWindow *)childwin ordered:(NSWindowOrderingMode)place;
- (void)removeChildWindow:(NSWindow *)childWin;
- (NSArray *)childWindows;
- (NSWindow *)parentWindow;
- (void)setParentWindow:(NSWindow *)window;
方法参数
对于方法参数的命名有一些通用规则:
- 跟随方法的参数,以些小字母开头,在它之后的单词首字母要大写。(比如:
removeObject:(id)anObject
)。 - 不用再命名里使用"pointer"或者"ptr"。使用参数的类型而不是它是不是一个指针。
- 避免使用"one-"和"tow-letter"这种形式为参数命名。
- 避免使用缩写导致只有几个字母。
一般来说,一下关键字和参数经常一起使用:
...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
...keyEquivalent:(NSString *)charCode
...length:(int)numBytes
...point:(NSPoint)aPoint
...stringValue:(NSString *)aString
...tag:(int)anInt
...target:(id)anObject
...title:(NSString *)aString
私有方法
大多数情况下,私有方法的命名与工友方法的命名是一样的。但是一个通用的做法是给私有方法一个前缀,使得能够更简单的把它与公有方法区别开来。即使在这种情况下,这种给私有方法命名的方式也会导致奇怪的问题。当你设计一个Cocoa框架类下的子类的时候,你不知道你的私有方法是不是无意中重写了框架里的私有方法。
Cocoa框架中的大多数私有方法都有一个下划线的前缀(比如:_fooData)来标记他们是私有方法。基于这个事实有两条建议。
- 不要使用下划线来左右你私有方法的前缀。苹果公司保留这个规范。
- 如果你子类化一个大的Cocoa框架的一个子类(比如NSView或者UIView),并且你确定你的私有方法的命名与父类不同,你可以给你的私有方法添加不同的前缀。前缀应该竟可能唯一,可能是一个基于你公司或项目的类似有"XX_"的格式。比如工程名"Byte Flogger",前缀可以是
BF_addObject:
虽然给私有方法一个前缀的命名可能与早前在为类命名的规则相矛盾,这这里的意图是不一样的:防止无意间重写父类的私有方法。
函数命名
Objective-C允许你使用函数跟方法来表达行为。当潜在的对象总是一个单例或者当你解决一个明显的函数式子系统的时候,你应该使用函数而不是说类方法。
函数命名有如下一些规范你需要遵守;
- 函数命名的格式与方法命名差不多,但是有一些例外:
- 在类跟常数的使用上,它们以同样的前缀开头。
- 前缀后的第一个单词是大写。
- 大多数以动词开头的函数描述了该函数的行为:
NSHighlightRect
NSDeallocateObject
查询属性的函数有额外的命名规则:
- 如果函数返回它第一个参数的属性,省略动词。
unsigned int NSEventMaskFromType(NSEventType type)
float NSHeight(NSRect aRect)
- 如果通过引用返回值,使用"Get"。
const char *NSGetSizeAndAlignment(const char *typePtr, unsigned int *sizep,unsigned int *alignp)
- 如果返回值是一个Boolean,函数应该以一个判断动词开头。
BOOL NSDecimalIsNotANNumber(const NSDecimal *decimal)
属性与数据类型的命名
这个部分表述了声明属性,实例变量,常数,通知等的命名规范。
属性声明和实例变量
属性声明影响一个属性的访问方法,所以其规范与访问方法的命名大致相同。如果属性是以名词或动词表达,格式是:@property(...)
名词或动词类型;
例子:
@property(strong) NSString *title;
@property(assign) BOOL showsAlpha
如果属性声明的词汇是形容词,属性名省略"is"前缀,但是规范上需要给get方法加上"is",例如:
@property(assign, getter=isEditable)BOOL editable;
在大多数情况下,当你声明了一个属性的时候,你同时也生成了一个实例变量。
确保简明扼要的表述了属性的存储。一般来说,你不应该直接访问实例变量;正确的做法是你应该使用访问方法(你在init
和dealloc
方法里直接访问实例变量)。为了标记这个,使用"-"前缀来标记实例变量,比如:
@implementation MyClass{
BOOL _showsTitle;
}
如果你使用属性声明来合成实例变量,在@synthesize
中指定实例变量的名字。
@implementation MyClass
@synthesize showsTitle = _showsTitle;
在把一个实例变量添加到一个类的时候,有一些需要考虑的东西要记住:
- 避免直接声明公共实例变量。
开发者应该把他们考虑成一个对象的接口,而不是它数据的组合细节。你可以使用属性声明和合成相应的实例变量来避免直接声明实例变量。 - 如果你需要声明一个实例变量,可以使用
@provate
或者@protected
来声明它.
如果你希望你的类会被子类化,并且子类将需要直接访问数据,可以使用@protected
来修饰。
如果一个实例变量可被类的实例访问,确保你为它写了访问方法。(可能的话,尽量使用属性声明)。
常数
常数的命名规则根据它创建的方式而不同。
枚举常量
- 使用枚举来关联一组有联系常量整数。
- 枚举常量与
typedef
遵守函数的命名规范。以下例子来做NSMatrix.h
:
typedef enum _NSMatrixMode{
NSRadioModeMatrix = 0,
NSHighlightModeMatrix = 1,
NSListModeMatrix = 2,
NSTrackModeMatrix =3,
} NSMatrixMode;
注意typedef
标示(_NSMatrixMode
在上面的例子里)是没有必要的。
- 你可以创建不具名的枚举,比如位掩码,比如:
enum {
NSBorderlessWindowMask = 0,
NSTitledWindowMask = 1 << 0,
NSClosableWindowMask = 1 << 1,
NSMiniaturizableWindowMask = 1 << 2,
NSResizableWindowMask = 1 << 3
};
使用const创建的常量
- 使用
const
来为浮点型创建常量。如果这个常量不与其他常量关联,你可以使用const
来创建一个interger类型的常量;其他情况,可以使用枚举。 -
const
类型的常量所遵循格式的一个例子:
const float NSLightGray
对于枚举常量,其命名规范与函数的命名一样。
其它类型的常量
- 一般情况下,不要使用
#define
的预处理命令来创建常量。对于interger类型的常量,使用枚举,对于浮点型指针使用const
,如上边所述。 - 使用大写字母定义预处理宏,来确定一块代码是否会被处理。例如:
#ifdef DEBUG
- 需要注意的是,编译器的宏定义头尾都有双下划线。例如:
__MACH__
- 字符串类型的常量通常被用作通知名和字典的key。通过使用字符串常量,你能确保编译器识别的值是正确的(就是说,它有拼写检查)。cocoa框架提供了很多字符串常量的常数。例如:
APPKIT_EXTERN NSString * NSPrintCopies;
字符串常量的值将会在实现文件里分配。(注意appkit_extern
宏定义在Objective-C中等价于extern
)
通知和异常
通知与异常的命名遵循相似的规则。但是他们有自己的被建议的使用模式。
通知
如果一个类有代理,它的多数通知将会通过定义一个代理方法来让代理接受。这些通知的名字应该能够反映相应的代理方法。例如,由于全局的NSApplication
对象注册了一个applicationDidBecomeActive:
方法,所以不论何时当程序发出NSApplicationDidBecomeActiveNotification
通知,它都能收到消息。
通知由如下形式的全局NSString对象标示:
[Name of associated class] + [Did | will] +[UniquePartOfName] + Notification
例如:
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification
NSColorPanelColorDidChangeNotification
异常
虽然你可以*的使用异常(就是说,由NSException
类和其相关函数提供的机制)来为你选择的如何目的,Cocoa保留编码错误的异常。比如数组越界。Cocoa没有使用异常来解决常规的,可预料的错误条件。在这些情况下,使用nil
,NULL
,NO
或者错误代码来做返回值。更多细节,请查看错误处理指南。
异常由如下形式的全局NSString对象表示:
[Prefix] + [UniquePartOfName] + [Exception]
名称的唯一部分应当由应当由几个单词组成,并且首字母大写。这是几个例子:
NSColorListIOException
NSColorListNotEditableException
NSDraggingException
NSFontUnavailableException
NSIllegalSelectorException
可接受的缩写和首字母缩略词
一般来说,当你设计编码接口的时候,你不应该使用缩写名。然而以下的缩写列表在过去被使用,你现在仍然可以继续使用它们。使用缩写的时候有一些事你必须要知道:
- 过去C库有一些已经用了很久的缩写,比如
alloc
、getc
是被允许的。 - 你可能在参数名上更多的使用缩写(比如,"obj","imageRep")。
缩写 | 本来的单词 |
---|---|
alloc | Allocate |
alt | Alternate |
app | Application |
calc | Calculate |
dealloc | Deallocate |
func | Function |
horiz | Horizontal |
info | Information |
init | initialize |
int | integer |
max | Maximum |
min | Minimum |
msg | Message |
nib | Interface Builder archive |
pboard | Pasteboard |
rect | Rectangle |
Rep | Representation |
temp | Temporary |
vert | Vertical |
你可能在电脑工程里使用缩写和首字母缩略词取代他们。这里有一些广为人知的首字母缩写:
ASCII
PDF
XML
HTML
URL
RTF
HTTP
TIFF
JPG
PNG
GIF
LZW
ROM
RGB
CMYK
MIDI
FTP
给框架开发者的一些小技巧跟技术
框架开发者在编写代码的时候必须必其他开发者更加注意。其他的客户端应用可以连接到他们的框架,正因如此,框架里的任何不足都可能通过一个系统方法。你可以采用以下讨论到的编程技术来保证你框架的效率跟完整性。
注意:这里面的有些技术不仅限于框架。你可以把它们运用于应用的开发当中
初始化
以下建议包含框架的初始化。
类的初始化
initialize
类方法给你一个只执行一次代码的地方。它是懒加载的,在其它类的方法之前调用。它一般被用用类的版本号设置。
runtime会给继承链的每个类发送initialize
方法。即使它并没有实现它。因此它可能不止一次的调用类的initialize
方法。(比如,一个子类并没有实现它)。一般来说,你想要初始化代码只执行一次。有一种方法来保证这个过程就是使用dispatch_once()
:
+ (void)initialize{
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
//初始化代码
}
}
注意:由于runtime给每个类发送初始化方法,如果子类没有实现initialize
方法,initialize
可能会在子类的上下文里被调用。那么这个调用就会让父类来调用。如果你确实想在相关类的上下文里初始化,你可以使用如下检查,而不是使用dispatch_once():
if (self == [NSFoo class]){
// the initializing code
}
你不应该手动调用initialize
方法。如果你需要触发初始化,调用一些无害的方法,比如:
[NSImage self];
指定初始化
指定初始化是一个类的init
方法调用父类的init
方法。(其他初始化者调用这个类的init
方法)每个公共类都应该有至少一个指定初始化方法。指定初始化的一个例子是NSView
的 initWithFrame
和NSResponder
的init
方法。这并不意味着init
方法需要被重写,有一个例子就是NSString
和其它类簇的抽象类,子类被期望于实现它自己。
指定初始化方法应该很容易被识别,因为它的信息对于想要子类化你的类的人来说是非常重要的。子类可以只重写指定初始化方法给你并且其它的初始化将会像设计一样的执行。
当你实现一个框架的类,你经常会去实现像是initWithCoder:
和encodeWithCoder:
这类归档方法。注意,当对象解档没有完成的时候 ,不要在初始化代码路径下做其他事情。如果你的类实现了归档,那么,从你的指定初始化方法和initWithCoder:
(它本身就是指定初始化方法)来实现将是一个不错的方式。
初始化过程中的错误检查
一个好的初始化方法设计应该完成以下这些步骤来保证恰当的错误检查和错误误差:
- 通过调用父类的指定初始化方法来再分配自己。(就是类似于调用
self = [super init]
) - 返回值为
nil
的检查,其指示了父类初始化期间发生了一些错误。 - 初始化当前类的时候如果出现了错误,release当前对象,并且返回
nil
。
一下解释了你能怎么做:
- (id)init {
self = [super init]; // Call a designated initializer here.
if (self != nil) {
// Initialize object ...
if (someError) {
[self release];
self = nil;
}
}
return self;
}
版本控制与兼容性
当你给你的框架添加一个新的类或者方法,给每个新特性组指定新版本号是没有必要的。开发者通常调用0bjective-C的runtime来检查。比如使用respondsToselector:
方法来判定在指定版本下某个特性是否有效。这些runtime测试是首先且是最动态的方式来检查新特性。
当然,你可以雇佣一些技术人员来保证你框架的每个新特性被恰当的标记,并且尽可能与早期版本有相同的兼容性。
框架版本
当现存的新特性或bug修复不能简单的时候runtime检测出来,你应该为开发者提供一些检查改变的方法。其中一个方法就是存储一个当前框架的额外版本号,并使开发者能够拿到这个版本号。
- 文档根据版本号改变。
- 给你的框架设置一个版本,并且提供一个全局都能取到它的方法。你可能在你框架的详情属性列表(info.plist)存一个版本号,然后就可以通过它拿到了。
键入存档
如果你的框架需要被写入到nib文件中,它们必须可以归档它们自己。你也需要归档机制把所有文档归档来保存文档数据。
关于归档,你应该考虑以下问题:
- 如果存档中没有某个key,调用它的值将会返回
nil
,NULL
,NO
,0或者0.0,取决于被调用者的类型。对返回值做测试以便减少你写出的数据。此外,你可以找到一个key是否被写入存档中。 - 归档与解档方法都可以做一些事情阿里保证后面的兼容性。例如,新版本类的归档方法可能使用一个key写入一个新值,但是仍然可以写入旧文件以便旧版本的类仍然可以识别对象。此外,解档方法可能想要通过一些合理的方法保留一些没有的值,以便将来的版本能够保持灵活性。
- 给框架的类的存档key的命名规范建议是:以被用于框架里其他API元素的缩写开头,然后使用实例便利的名字。只要保证名字不与父类或子类冲突即可。
- 如果你有一个函数用来编写基本的数据类型,确保使用唯一的值。例如,一个“archiveRect”通常存档一个长方形,应当带一个key参数,或者使用它。或者,如果它写入多个值(比如,四个floats),它应当给提供的key添加它自己唯一的比特。
- 由于编译器和字节序的依赖,归档位字段可能是危险的。只有当遇到性能问题,许多bits需要被写入很多次的时候才能归档它们。可以查看位字段的建议。
异常和错误
大多数的cocoa框架并不强迫开发者去捕捉和解决异常。这是因为异常并不是作为正常执行的一部分而被创建。而且,它通常并不被用于传递预期的runtime或用户错误。这些错误的例子包含:
- 找不到文件
- 找不到该用户
- 在app中打开一个错误格式的文件
- 字符串转码错误
然而,cocoa创建异常来指示编码错误或者逻辑错误,比如: - 数组越界
- 尝试操作不可变的对象
- 错误的参数类型
值得期望的是,开发者会在测试的时候捕获到这些错误,并在app发布前解决这些错误。因此app不需要在运行时解决这些异常。一个产生了一个异常,但是程序没有捕捉到它,最高等级的默认解决者一般会捕获、报告、然后继续执行。开发者可以选择替代默认的异常。捕获者给予更多错误信息,并且提供保存数据和终止程序的选项。
错误是cocoa框架与其他软件库不同的另一个地方。cocoa方法一般不返回错误代码。再有一个合理或简单理由的错误原因情况下,方法依靠简单的布尔测试或者对象(nil/non-nil)返回值。记录了NO
或nil
返回值的原因。您不应该使用错误代码来指示要在运行时处理的编程错误,而是产生异常,或者在某些情况下仅仅记录错误而不引发异常。
比如,NSDictionary
的objectForKey:
方法会返回找到的对象,没找到就会返回nil
。NSArray
的objectAtIndex:
方法永远都不会返回nil
,因为NSArray
对象不能存储nil
值,并且通过定义访问越界的异常来指示编程错误当对象不能通过提供的参数正确的初始化的时候,许多init
方法会返回nil
。
在一些情况下,一些方法会有不同的错误代码。它应该在一个引用参数中指定它们,该参数返回一个错误代码,一个本地化的错误字符串,或一些描述错误的其他信息.例如:你可能想要返回一个当做NSError
的对象,在Foundation中查看NSError.h
头文件的详情。这个参数可能是更简单的BOOL
或者nil
直接返回。该方法还应遵守约定,所有参考引用参数是可选的,因此如果发送方不希望知道错误,则允许发送方为错误代码参数传递NULL。
框架数据
你如何处理框架数据对性能,跨平台兼容性和其他目的有影响。这个部分将讨论包括框架数据的技术。
常量数据
由于性能的原因,尽可能的把框架数据标记为使用常量,因为这样做会减小Mach-O
二进制的__DATA
段的大小。不是const
的全局和静态数据会在__DATA
段的__DATA
部分结束。这种数据在每个使用此框架的运行实例中占用内存。虽然多500字节的内存占用可能没关系,但是它可能会导致所需的页面数量增加 - 每个应用程序额外增加4千字节。
你应该把任何常量数据标记为const
。如果在block里有char *
,这将会导致数据降落在__TEXT
字段。此外,它将会待在__DATA
字段但是不睡写入。(除非预加载没有完成或者由于在加载时滑动二进制而被违反)。
你应该初始化静态变量来保证他们被合并入__DATA
字段的__data
部分而不是__bss
部分。如果没有明显的值用于初始化,使用0,NULL,0.0或者任何恰当的值。
位字段
如果代码假定值为布尔值,则对位字段使用有符号值,尤其是使用一位位域,可能会导致未定义的行为。 一位位域应始终为无符号。 因为可以存储在这样的位字段中的唯一值是0和-1(取决于编译器实现),将该位域与1进行比较是假的。 例如,如果你在代码中遇到类似这样的东西:
BOOL isAttachment:1;
BOOL startTracking:1;
你应该把类型转为无符号的int。
位字段的另一个问题是归档。 通常,您不应以位格式将位字段写入磁盘或归档,因为在另一个架构或另一个编译器上再次读取时,格式可能不同。
内存分配
在框架代码中,最好的方法是避免分配内存,如果你可以帮助。如果你因为一些理由需要临时缓存,通常来说,使用堆栈比分配缓存更好。当然,堆栈会限制大小(通常是512千比特),所以决定是否使用堆栈取决于功能和你需要的缓存大小。一般来说,如果你需要1000比特以下的缓存大小,使用堆栈是可接受的。
一个改进是开始使用堆栈,但是如果大小要求超过堆栈缓冲区大小,则切换到malloc'ed缓冲区。 以下提供了一个代码片段:
#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
方法相关联。对于放置在基于散列的Cocoa集合(例如NSDictionary或NSSet)中的对象的一个基本不变量是,如果[A isEqual:B] == YES,则[A hash] == [B hash]。所以如果你在你的类中重写isEqual:,你也应该覆盖hash来保留这个不变量。 默认情况下isEqual:
查找每个对象的地址的指针相等,并且hash
基于每个对象的地址返回一个哈希值,所以这个不变量成立。
第一次翻译 ==
问题应该比较多,参考了一些别人的翻译,同时还有谷歌的翻译
不得不感叹,谷歌的翻译好牛逼。。。
发现错误,欢迎指出