Cocoa编码规范

介绍Cocoa编码规范

开发一个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方法。
    • 对于函数和常量的命名,对于相关的类使用相同的前缀,并且紧接着后面的一个单词首字母大写。例如:NSRunAlertPanelNSCellDisabled
    • 避免使用下划线字符作为私有方法的命名前缀(对于使用下划线字符作为实例变量的命名前缀是允许的)。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;

以下是对这个指南的限制和改进:

  • 如果这个集合的确是无序的,返回一个NSSet对象,而不是一个NSArray对象。
  • 如果向一个集合中指定位置插入一个元素非常重要,采用以下形式的方法:
    - (void)insertLayoutManager:(NSLayoutManager *)obj atIndex:(int)index;
    - (void)removeLayoutManagerAtIndex:(int)index;
    

还有一些针对于MRC的说明,这里就不列举了。在非ARC下,确定好需要强弱引用。如果是强引用,使用NSArray、NSDictionary, NSSet等;如果是弱引用,则使用NSPointerArray、NSMapTable、NSHashTable。

方法参数

这里有一些设计方法参数命名的一般规则:

  • 和方法一样,参数要以一个小写字母打头,并且后续每一个单词的首字母要大写(例如: removeObject:(id)anObject)。
  • 不要在参数名中使用“pointer” 或“ptr”。让参数的类型而不是它的名称,声明它是否是指针。
  • 避免1-2个字母的参数名字。
  • 避免缩写

一般来说(在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 ),用来标记它是私有方法。基于这个事实,有以下两个建议:

  • 不要使用下划线字符作为私有方法的前缀。Apple保留了这一惯例。
  • 如果是一个大型Cocoa框架类(如NSViewUIView)的子类,你想要绝对确定你的私有方法的名称与超类中的名称不同,你可以在你的私有方法前面加上一个你自己的前缀。这个前缀尽可能的唯一,或许基于你的公司或者项目,诸如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)
    
  • 如果返回的是布尔类型,函数应该以一个变化动词打头。
    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

  • 如果一个实例变量是一个类实例的可访问属性,确保你给他写了读写方法(如果可能,请使用声明属性)。

常量

常量的命名规则根据常量的创建方式而有所不同。

枚举常量
  • 对具有整数值的相关常量组使用枚举。
  • 枚举常量和类型定义组的命名约定遵循函数的命名(参见函数命名)。以下例子来自 NSMatrix.h:
    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常量的命名格式的例子:

    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

可接受的缩写和缩略语

一般来说,你在设计你的编程接口的时候,不应当缩写名字(参见一般原则)。然而,以下列举的缩写词已经创建并且在过去使用了很久,所以你可以继续使用他。这里有额外的两件事情需要注意:

  • 简写格式和在标准的C库中长期使用的格式相同的,例如:“alloc”和“getc”是允许的。
  • 你在参数名中可以更加自由的使用简写(例如: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
    }
}
备注:因为运行时会发送初始化给每一个类,很有可能initialize在子类的上下文环境中调用。如果子类没有实现initialize,然后这个调用会传到父类。如果你需要在相关的类的上下文中执行初始化,你可以用以下检查替代 dispatch_once()的使用会更好:
if (self == [NSFoo class]) {
    // the initializing code
}

绝不要显示的调用initialize方法。如果你想要触发初始化,调用一些无害的方法,例如:

[NSImage self];

指定的初始化函数

指定的初始化函数是调用父类的init方法的类的init方法。(其它初始化器调用类定义的初始化方法)。每个公共类都应该有一个或多个指定的初始化函数。指定初始化方法的例子有:NSView类的initWithFrame:NSResponderinit方法。init方法并不意味着需要重写,比如NSString类和其它类簇中的抽象类,子类应该来自己实现。

指定的初始化函数应当明确的指定,因为这对于想要依据你的类创建子类来说非常重要。子类仅重写指定的初始化函数。

当你要实现一个框架的类时,你经常需要也实现它的存档方法:initWithCoder:encodeWithCoder:。注意不要当在对象解归档的时候,在初始化的代码中执行不会发生的事情。实现这一目标的一个好方法是,如果您的类实现了归档,则从您指定的初始化程序和initWithCoder调用一个公共例程:(这是一个指定的初始化函数)。

初始化过程中的错误检测

一个设计比较好的初始化方法应当通过完成以下几部来确保正确的检测和错误输出:

  1. 通过调用super的指定初始化程序重新分配自己。
  2. 检查nil返回值,这个表明在父类初始化的过程中发生了错误。
  3. 如果在初始化当前类对象的时候,发生错误,释放这个对象并返回nil。
//初始化过程中的错误检测
- (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被修复时,通过运行时测试是不容易检测出来的,你应当以某种方式告知开发者来检测这种变化。一种方式就是以归档的形式来存储框架的确切版本号,同时需要让开发者可见这些内容:

  • 在每一个版本号下做文档记录变化(例如,在发布备注中)。
  • 设置你框架当前的版本号并且提供一些方式让它能够全局访问。你可以通过Info.plist文件来存储你框架的版本号,然后可以通过这种方式来访问。

归档中的key

如果你的框架对象需要写入nib文件,他们必须能够自归档。你同样需要通过使用归档机制存储文档数据来归档任何文档。

你应当考虑以下关于归档方面的问题:

  • 如果归档中的key丢失了,请求对应的值的话,将会返回nil、NULL、NO、0或0.0等,取决于请求的数据类型。通过判断这个返回值,可以减少你的数据输出。另外,你可以确认这个key有没有被写入归档。
  • 编码和解码方法都可以做到确保向后兼容性。例如一个类的新版本的编码方法可能会通过使用key写入新的值,但是可能仍然返回旧的字段以便旧版本的类仍然知道这个对象。另外,解码方法想要通过一些可能的方式来处理丢失的值来保持新版本的灵活性。
  • 对于框架类的归档key的一个推荐的命名约定就是以针对于其他框架API元素的前缀并且使用实例变量的名字。这样确保命名不会和其他任何父类或子类的名字冲突。
  • 如果你有一个工具函数输出一个基本的数据类型(换言之,这个值不是对象),确保使用一个唯一的key。例如,如果你有一个archiveRect程序来归档举行,需要传入key参数,你可以使用它。或者,如果它输出多个值(例如,四个浮点数据),应当在给定的key上追加自己唯一的位。
  • 按照原样来归档位字段是很危险的,因为这个和编译器以及字节顺序依赖有关。你仅能在对于优化的原因的情况下归档位字段,例如,需要大量多次的的位输出。参见位字段。

异常和错误

大多数Cocoa框架方法都不会强制开发人员捕获和处理异常。这是因为异常不是作为执行的正常部分引发的,并且通常不用于传达预期的运行时或用户错误。这些错误的示例包括:

  • 文件没有找到
  • 没有此用户
  • 在应用中视图打开一个错误的文档类型
  • 转化字符串到特定编码格式错误

然而,Cocoa对于以下情况会产生异常来指明程序或者逻辑错误:

  • 数据越界访问
  • 尝试改变不可变的对象
  • 错误的参数类型

期望开发人员在测试期间捕获这些错误并在发布应用程序之前解决它们;因此,应用程序不需要在运行时处理异常。如果一个异常往外扩散,应用没有捕获它,高级别的默认处理器通常会处理它,并且会报告异常,然后让它们继续执行。开发人员可以选择将此默认异常捕获器替换为提供更多错误详细信息的异常捕获器,并且提供一个可选项来保存数据并且退出应用程序。

错误是Cocoa框架与其他软件库不同的另一个领域。Cocoa方法通常不返回错误代码。在存在一个合理或可能的错误原因的情况下,这些方法依赖于对布尔或对象(nil / non-nil)返回值的简单测试;记录NO或零返回值的原因。您不应该使用错误代码来指示要在运行时处理的编程错误,而是引发异常,或者在某些情况下只记录错误而不引发异常。

例如,NSDictionaryobjectForKey:方法要么返回找到的对象,要么返回nil,如果它找不到对象。NSArrayobjectAtIndex:方法不会返回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方法有关。对象一个最基本的不变的地方就是被放入一个基于哈希的例如NSDictionaryNSSetCocoa集合中,如果[A isEqual:B] == YES,那么[A hash] == [B hash]。如果你重写你的类的isEqual:方法,你应当也要重写hash方法来维持这一个不变的条件。默认的isEqual:方法寻找和每一个对象地址相等的指针,并且hash方法返回的hash值是基于每一个对象的地址,所以还是保持了这个不变性。

发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/88649138