iOS学习iOS Developer

cocoa编码指南(翻译)

2017-01-15  本文已影响131人  酱油不爱醋

开发cocoa框架、插件或者其它带公共API的可执行文件需要不同于应用开发一些方法和惯例。你的产品的主要客户是开发人员,这些人员不会对你的接口迷惑是很重要的。这时候,API的命名规则就将派上用场,它可以帮助你让你的接口清晰、一致。有些编程技术对framework来说是特殊或者极其重要的,比如版本、兼容性、错误处理和内存管理。这个话题包含了Cocoa的命名规则和framework的编程练习建议。

文档结构

本文的主题包含两种基本类型。第一种也是最大的一组是编程接口的命名规则。这是与Apple应用再它自己的Cocoa框架上相同的规则。(除了少数例外)这些命名规则的文章包含一下内容:

第二种讨论framework编程的方法(目前成员之一):

代码命名基础

面向对象的软件库的设计一个经常被忽视的方面是类、方法、函数、常量和编程接口的其他元素的命名。这个部分讨论一些对多数Cocoa接口通用的命名规则。

一般原则

清晰

代码 评价
insertObject:atIndex: Good
insert:at: 不清晰,插入的是什么?at表示什么
removeObjectAtIndex: Good
removeObject: Good,它移除参数中提到的对象
remove: 不清晰,什么将要被移除?
代码 评价
destinationSelection Good
destSel 不清晰
setBackgroundColor: Good
setBkgColor: 不清晰

你可能认为某个缩写是大家都知道的,但是它可能不是。特别是遇到有不同文化或语言背景的开发者调用你的方法或者函数。

代码 评价
sendPort 它表示发送一个接口还是返回?
displayName 它是显示一个名字还是在用户界面里返回接收者的标题?

一致性

代码 评价
- (NSInteger)tag 在NSView,NSCell,NSControl中定义过
- (void)setStringValue:(NSString *) 在很多Cocoa类里定义过

你也可以查看方法参数

无自身参照

代码 评价
NSString Okay.
NSStringObject 自我涉及
代码 评价
NSUnderlineByWordMask Okay.
NSTableViewCalumnDidMoveNotification Okay

前缀

在编程接口名中,前缀是非常重要的。它们可以区分软件的不通功能区域。通过这个软件被打包成一个framework或者与framework相近的东西。第三方开发者和苹果定义的前缀标识能够防止冲突。(当然也包括苹果的framework之间的标识)

前缀 Cococa Framework
NS Foundation
NS Application Kit
AB Address Book
IB Interface Builder

在为类、协议、函数、常量和typedef结构体中命名时使用前缀。不要在为方法命名时使用前缀;方法存在于定义他们的类的命名空间中。当然,也不要在文件命名时使用前缀。

书写规则

下面是一些命名API元素时的一些书写规则

类和协议命名

类名应该包含一个对类的清晰的表达或者要做什么的名词。命名应该有一个恰当的前缀(参考前缀)。Foundation和application框架里充满了例子;比如:NSString,NSDate,NSScanner,NSApplication,UIApplication,NSButton和UIButton。
协议(Protocols)应当根据它的分组行为来命名

代码 评价
NSLocking Good
NSLock Poor(像一个类的名字)

头文件

如何给头文件命名是非常重要的,因为通过合理的习惯,你用来指示这些文件包含的内容:

头文件 声明
NSLocale.h NSLocale类
头文件 声明
NSString.h NSString和NSMutableString类
NSLock.h NSLocking协议和NSLock,NSConditionLock和NSRecursiveLock类
头文件 Framework
Foundation.h Foundation.framework

方法命名

方法可能是你程序界面里最普遍的元素了,所以你应该尤其注意如何给它们命名。这个部分我们方法方面的命名。

一般规则

这里有一些需要在方法命名里常用的指南需要记住:

- (void)invokeWithTarget:(id)target;
- (void)selectTabViewItem:(NSTabViewItem *)tabViewItem;

不要使用"do"或"dose"作为命名的一部分,因为这些辅助动词很少有添加的意义。同时,也不要再动词前面使用副词跟形容词。

代码 评价
- (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"在这个例子里听起来很不错,但是当你创建了越来越多的关键字的时候就会出现问题。

代码 评价
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag; NSWorkspace

访问方法

"访问方法"即是对象的属性的设置、获取方法。它们有推荐的格式,取决于其属性如何表达:

例子:

- (NSString *)title;
- (void)setTitle:(NSString *)aTitle;

例子:

- (BOOL)isEditable;
- (void)setEditable:(BOOL)flag;

例子:

- (BOOL)showsAlpha;
- (void)setShowsAlpha:(BOOL)flag;

动词应该使用现代时。

代码 评价
- (void)setAcceptsGlyphInfo:(BOOL)flag; 正确
- (BOOL)acceptsGlyphInfo; 正确
- (void)setGlyphInfoAccepted:(BOOL)flag; 错误
- (BOOL)glyphInfoAccepted; 错误
代码 评价
- (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;
- (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;

集合方法有很多实现细节需要记住:

- (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;

方法参数

对于方法参数的命名有一些通用规则:

...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)来标记他们是私有方法。基于这个事实有两条建议。

函数命名

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 NSDecimalIsNotANNumber(const NSDecimal *decimal)

属性与数据类型的命名

这个部分表述了声明属性,实例变量,常数,通知等的命名规范。

属性声明和实例变量

属性声明影响一个属性的访问方法,所以其规范与访问方法的命名大致相同。如果属性是以名词或动词表达,格式是:
@property(...)名词或动词类型;
例子:

@property(strong) NSString *title;
@property(assign) BOOL showsAlpha

如果属性声明的词汇是形容词,属性名省略"is"前缀,但是规范上需要给get方法加上"is",例如:

@property(assign, getter=isEditable)BOOL editable;

在大多数情况下,当你声明了一个属性的时候,你同时也生成了一个实例变量。
确保简明扼要的表述了属性的存储。一般来说,你不应该直接访问实例变量;正确的做法是你应该使用访问方法(你在initdealloc方法里直接访问实例变量)。为了标记这个,使用"-"前缀来标记实例变量,比如:

@implementation MyClass{
    BOOL _showsTitle;
}

如果你使用属性声明来合成实例变量,在@synthesize中指定实例变量的名字。

@implementation MyClass
@synthesize showsTitle = _showsTitle;

在把一个实例变量添加到一个类的时候,有一些需要考虑的东西要记住:

如果一个实例变量可被类的实例访问,确保你为它写了访问方法。(可能的话,尽量使用属性声明)。

常数

常数的命名规则根据它创建的方式而不同。

枚举常量

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 float NSLightGray

对于枚举常量,其命名规范与函数的命名一样。

其它类型的常量

#ifdef DEBUG
__MACH__
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

可接受的缩写和首字母缩略词

一般来说,当你设计编码接口的时候,你不应该使用缩写名。然而以下的缩写列表在过去被使用,你现在仍然可以继续使用它们。使用缩写的时候有一些事你必须要知道:

缩写 本来的单词
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方法)每个公共类都应该有至少一个指定初始化方法。指定初始化的一个例子是NSViewinitWithFrameNSResponderinit方法。这并不意味着init方法需要被重写,有一个例子就是NSString和其它类簇的抽象类,子类被期望于实现它自己。
指定初始化方法应该很容易被识别,因为它的信息对于想要子类化你的类的人来说是非常重要的。子类可以只重写指定初始化方法给你并且其它的初始化将会像设计一样的执行。
当你实现一个框架的类,你经常会去实现像是initWithCoder:encodeWithCoder:这类归档方法。注意,当对象解档没有完成的时候 ,不要在初始化代码路径下做其他事情。如果你的类实现了归档,那么,从你的指定初始化方法和initWithCoder:(它本身就是指定初始化方法)来实现将是一个不错的方式。

初始化过程中的错误检查

一个好的初始化方法设计应该完成以下这些步骤来保证恰当的错误检查和错误误差:

  1. 通过调用父类的指定初始化方法来再分配自己。(就是类似于调用self = [super init])
  2. 返回值为nil的检查,其指示了父类初始化期间发生了一些错误。
  3. 初始化当前类的时候如果出现了错误,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检测出来,你应该为开发者提供一些检查改变的方法。其中一个方法就是存储一个当前框架的额外版本号,并使开发者能够拿到这个版本号。

键入存档

如果你的框架需要被写入到nib文件中,它们必须可以归档它们自己。你也需要归档机制把所有文档归档来保存文档数据。
关于归档,你应该考虑以下问题:

异常和错误

大多数的cocoa框架并不强迫开发者去捕捉和解决异常。这是因为异常并不是作为正常执行的一部分而被创建。而且,它通常并不被用于传递预期的runtime或用户错误。这些错误的例子包含:

框架数据

你如何处理框架数据对性能,跨平台兼容性和其他目的有影响。这个部分将讨论包括框架数据的技术。

常量数据

由于性能的原因,尽可能的把框架数据标记为使用常量,因为这样做会减小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方法运行你通过任意参数所谓参数,如果对象不是同一个类,则返回NOisEqualToString:isEqualToArray:方法经常用于参数是确定的类型。因此他们没有做类型检查,所以他们很快,但是并不安全。从外部资源取回的值,比如从应用的属性列表(info.plist),使用isEqual:会更好,因为它比较安全。当类型是已知的,使用isEqualToString:来替换。
关于isEqual:的另一点是,它与hash方法相关联。对于放置在基于散列的Cocoa集合(例如NSDictionary或NSSet)中的对象的一个基本不变量是,如果[A isEqual:B] == YES,则[A hash] == [B hash]。所以如果你在你的类中重写isEqual:,你也应该覆盖hash来保留这个不变量。 默认情况下isEqual:查找每个对象的地址的指针相等,并且hash基于每个对象的地址返回一个哈希值,所以这个不变量成立。

第一次翻译 ==
问题应该比较多,参考了一些别人的翻译,同时还有谷歌的翻译
不得不感叹,谷歌的翻译好牛逼。。。
发现错误,欢迎指出

上一篇 下一篇

猜你喜欢

热点阅读