Objective-C 编码规范
前言
Objective-C 是 C 语言的扩展,增加了动态类型和面对对象的特性。它被设计成具有易读易用的,支持复杂的面向对象设计的编程语言。它是 Mac OS X 以及 iPhone 的主要开发语言。本文档目标是统一开发人员的编码风格,使项目容易被理解以及后期维护。
1留白和格式
1.1空格 VS 制表符
【建议】使用空格进行缩进。
不要在工程里使用Tab键,使用空格来进行缩进。
通过设置 Xcode > Preferences > Text Editing 将Tab和自动缩进都设置为4个空格。
- 类方法声明在方法类型与返回类型之间要有空格。
// 糟糕
-(void)methodName:(NSString *)string;
// OK
- (void)methodName:(NSString *)string;
- 条件判断的括号内侧不应有空格。
// 糟糕
if ( a < b ) {
// something
}
// OK
if (a < b) {
// something
}
- 关系运算符(如 >= 、 != )和逻辑运算符(如 && 、 || )两边要有空格。
// OK
(someValue > 100)? YES : NO
// OK
(items)?: @[]
二元算数运算符两侧是否加空格不确定,根据情况自己定。一元运算符与操作数之前没有空格。
多个参数逗号后留一个空格(这也符合正常的西文语法)。
1.2 花括号
【建议】方法大括号和其他大括号(if/else/switch/while 等.)总是在同一行语句打开但在新行中关闭。
if (user.isHappy) {
//Do something
}
else {
//Do something else
}
1.3列数
【建议】尽量让你的代码保持在 80 列之内。
通过设置 Xcode > Preferences > Text Editing > Show page guide,来使越界更容易被发现。
1.4 方法的声明和定义
【建议】 +/- 和返回类型之间须使用一个空格,参数列表中只有参数之间可以有空格。
例:
+ (NSDate *)dateFromString:(NSString *)string{
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSDate *date = [formatter dateFromString:string];
[formatter release];
return date;
}
星号前的空格是可选的。当写新的代码时,要与先前代码保持一致。如果一行有非常多的参数,更好的方式是将每个参数单独拆成一行。如果使用多行,将每个参数前的冒号对齐。
- (void)doSomethingWith:(GTMFoo *)theFoo
rect:(NSRect)theRect
interval:(float)theInterval {
...
}
当第一个关键字比其它的短时,保证下一行至少有 4 个空格的缩进。这样可以使关键字垂直对齐,而不是使用冒号对齐:
- (void)short:(GTMFoo *)theFoo
longKeyword:(NSRect)theRect
evenLongerKeyword:(float)theInterval {
...
}
1.5 方法调用
【建议】方法调用应尽量保持与方法声明的格式一致。当格式的风格有多种选择时,新的代码要与已有代码保持一致。
调用时所有参数应该在同一行:
[myObject doFooWith:arg1 name:arg2 error:arg3];
或者每行一个参数,以冒号对齐:
[myObject doFooWith:arg1
name:arg2
error:arg3];
方法定义与方法声明一样,当关键字的长度不足以以冒号对齐时,下一行都要以四个空格进行缩进。
[myObj short:arg1
longKeyword:arg2
evenLongerKeyword:arg3];
1.6 协议名
【建议】类型标识符和尖括号内的协议名之间,不能有任何空格。
这条规则适用于类声明、实例变量以及方法声明。例如:
@interface MyProtocoledClass : NSObject<NSWindowDelegate> {
@private
id<MyFancyDelegate> delegate_;
}
- (void)setDelegate:(id<MyFancyDelegate>)aDelegate;
@end
@public 和 @private 访问修饰符应该以一个空格缩进。
1.7 块(闭包)
【建议】块(block)适合用在 target/selector 模式下创建回调方法时,因为它使代码更易读。块中的代码应该缩进 4 个空格。
取决于块的长度,下列都是合理的风格准则:
- 如果一行可以写完块,则没必要换行。
- 如果不得不换行,关括号应与块声明的第一个字符对齐。
块内的代码须按 4 空格缩进。
- 如果块太长,比如超过 20 行,建议把它定义成一个局部变量,然后再使用该变量。
- 如果块不带参数,^{ 之间无须空格。如果带有参数,^( 之间无须空格,但 ) { 之间须有一个空格。
- 块内允许按两个空格缩进,但前提是和项目的其它代码保持一致的缩进风格。
// The entire block fits on one line.
[operation setCompletionBlock:^{ [self onOperationDone]; }];
[operation setCompletionBlock:^{
[self.delegate newDataAvailable];
}];
dispatch_async(fileIOQueue_, ^{
NSString* path = [self sessionFilePath];
if (path) {
// ...
}
});
[[SessionService sharedService]
loadWindowWithCompletionBlock:^(SessionWindow *window) {
if (window) {
[self windowDidLoad:window];
} else {
[self errorLoadingWindow];
}
}];
// An example where the parameter wraps and the block declaration does
// not fit on the same line as the name.
[[SessionService sharedService]
loadWindowWithCompletionBlock:
^(SessionWindow *window) {
if (window) {
[self windowDidLoad:window];
} else {
[self errorLoadingWindow];
}
}];
// Large blocks can be declared out-of-line.
void (^largeBlock)(void) = ^{
// ...
};
[operationQueue_ addOperationWithBlock:largeBlock];
1.8 枚举类型
【建议】当使用enum时,推荐使用新的固定基本类型规格,因为它有更强的类型检查和代码补全。现在SDK有一个宏NS_ENUM()来帮助和鼓励你使用固定的基本类型。
typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
RWTLeftMenuTopItemMain,
RWTLeftMenuTopItemShows,
RWTLeftMenuTopItemSchedule
};
1.9 Case语句
【建议】大括号在case语句中并不是必须的,除非编译器强制要求。当一个case语句包含多行代码时,大括号应该加上。
switch (condition) {
case 1:
// ...
break;
case 2: {
// ...
// Multi-line example using braces
break;
}
case 3:
// ...
break;
default:
// ...
break;
}
有很多次,当相同代码被多个cases使用时,一个fall-through应该被使用。一个fall-through就是在case最后移除'break'语句,这样就能够允许执行流程跳转到下一个case值。为了代码更加清晰,一个fall-through需要注释一下。
switch (condition) {
case 1:
// ** fall-through! **
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}
当在switch使用枚举类型时,'default'是不需要的。例如:
RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;
switch (menuType) {
case RWTLeftMenuTopItemMain:
// ...
break;
case RWTLeftMenuTopItemShows:
// ...
break;
case RWTLeftMenuTopItemSchedule:
// ...
break;
}
2命名
对于易维护的代码而言,命名规则非常重要。Objective-C 的方法名往往十分长,但代码块读起来就像散文一样,不需要太多的代码注释。
当编写纯粹的 Objective-C 代码时,我们基本遵守标准的 Objective-C naming rules,这些命名规则可能与 C++ 风格指南中的大相径庭。例如,Google 的 C++ 风格指南中推荐使用下划线分隔的单词作为变量名,而(苹果的)风格指南则使用驼峰命名法,这在 Objective-C 社区中非常普遍。
任何的类、类别、方法以及变量的名字中都使用全大写的 首字母缩写。这遵守了苹果的标准命名方式,如 URL、TIFF 以及 EXIF。
当编写 Objective-C++ 代码时,事情就不这么简单了。许多项目需要实现跨平台的 C++ API,并混合一些 Objective-C、Cocoa 代码,或者直接以 C++ 为后端,前端用本地 Cocoa 代码。这就导致了两种命名方式直接不统一。
我们的解决方案是:编码风格取决于方法/函数以哪种语言实现。如果在一个 @implementation 语句中,就使用 Objective-C 的风格。如果实现一个 C++ 的类,就使用 C++ 的风格。这样避免了一个函数里面实例变量和局部变量命名规则混乱,严重影响可读性。
2.1 文件名
【建议】文件名的命名应反映出其实现了什么类--包括大小写。遵循你所参与项目的约定。
文件的扩展名应该如下:
类别的文件名应该包含被扩展的类名,如:GTMNSString+Utils.h 或``GTMNSTextView+Autocomplete.h``。
2.2 Objective-C++
【建议】源代码文件内,Ojbective-C++ 代码遵循你正在实现的函数/方法的风格。
为了最小化 Cocoa/Objective-C 与 C++ 之间命名风格的冲突,根据待实现的函数/方法选择编码风格。实现 @implementation 语句块时,使用 Objective-C 的命名规则;如果实现一个 C++ 的类,就使用 C++ 命名规则。
// file: cross_platform_header.h
class CrossPlatformAPI {
public:
...
int DoSomethingPlatformSpecific(); // impl on each platform
private:
int an_instance_var_;
};
// file: mac_implementation.mm
#include "cross_platform_header.h"
// A typical Objective-C class, using Objective-C naming.
@interface MyDelegate : NSObject {
@private int instanceVar_;
CrossPlatformAPI* backEndObject_;
}
- (void)respondToSomething:(id)something;
@end
@implementation MyDelegate
- (void)respondToSomething:(id)something {
// bridge from Cocoa through our C++ backend
instanceVar_ = backEndObject->DoSomethingPlatformSpecific();
NSString* tempString = [NSString stringWithInt:instanceVar_];
NSLog(@"%@", tempString);
}
@end
// The platform-specific implementation of the C++ class, using
// C++ naming.
int CrossPlatformAPI::DoSomethingPlatformSpecific() {
NSString* temp_string = [NSString stringWithInt:an_instance_var_];
NSLog(@"%@", temp_string);
return [temp_string intValue];
}
2.3 类名
【建议】类名(以及类别、协议名)应首字母大写,并以驼峰格式分割单词。
应用层 的代码,应该尽量避免不必要的前缀。为每个类都添加相同的前缀无助于可读性。当编写的代码期望在不同应用程序间复用时,应使用前缀(如:GTMSendMessage)。
2.4 类别名
【建议】类别名应该包含它所扩展的类的名字。同时还应该表明该类别的功能。推荐如下方式:” UILabel+FlickerNumber.h”
2.5 Objective-C 方法名
【建议】方法名应该以小写字母开头,并混合驼峰格式。每个具名参数也应该以小写字母开头。
方法名应尽量读起来就像句子,这表示你应该选择与方法名连在一起读起来通顺的参数名。(例如,convertPoint:fromRect: 或 replaceCharactersInRange:withString:)。
访问器方法应该与他们 要获取的 成员变量的名字一样,但不应该以get作为前缀。
以 get 、 set 开头的方法有特殊的意义,不要随意定义。
- set 是属性默认的设置方法,如果函数不是为了设置类成员,则不要用 set 开头,可用 setup 替代。
- ii. get 和属性方法无关,但在 Cocoa 中,其标准行为是通过引用传值,而不是直接返回结果的。欲获取变量,直接以变量名为名,如: userInfomation ,而不是 getUserInfomation 。
例如:
- (id)getDelegate; // AVOID
- (id)delegate; // GOOD
这仅限于 Objective-C 的方法名。C++ 的方法与函数的命名规则应该遵从 C++ 风格指南中的规则。
2.6 变量名
【建议】变量名应该以小写字母开头,并使用驼峰格式。类的成员变量应该以下划线作为后缀。
例如:myLocalVariable、myInstanceVariable_。如果不能使用 Objective-C 2.0 的 @property,使用 KVO/KVC 绑定的成员变量可以以一个下划线作为前缀。
2.6.1 普通变量名
对于静态的属性(int 或指针),不要使用匈牙利命名法。尽量为变量起一个描述性的名字。不要担心浪费列宽,因为让新的代码阅读者立即理解你的代码更重要。例如:
2.6.2 实例变量
实例变量应该混合大小写,并以下划线作为后缀,如 usernameTextField_。然而,如果不能使用 Objective-C 2.0(操作系统版本的限制),并且使用了 KVO/KVC 绑定成员变量时,我们允许例外。这种情况下,可以以一个下划线作为成员变量名字的前缀,这是苹果所接受的键/值命名惯例。如果可以使用 Objective-C 2.0,@property 以及 @synthesize 提供了遵从这一命名规则的解决方案。
2.6.3 常量
常量名(如宏定义、枚举、静态局部变量等)应该以小写字母 k 开头,使用驼峰格式分隔单词,如:kInvalidHandle,kWritePerm。
2.7 通知命名
【建议】基本命名格式是: [与通知相关的类名] + [Did | Will] + [UniquePartOfName] + Notification 例如:
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification
NSColorPanelColorDidChangeNotification
2.8协议名
【建议】常用的 delegate、dateSource 做结尾外,还可以使用 …ing 这种形式,如:NSCoding、NSCopying、NSLocking
3注释
3.1 声明部分的注释
【建议】每个接口、类别以及协议应辅以注释,以描述它的目的及与整个项目的关系。
// A delegate for NSApplication to handle notifications about app
// launch and shutdown. Owned by the main app controller.
@interface MyAppDelegate : NSObject {
...
}
@end
如果你已经在文件头部详细描述了接口,可以直接说明 “完整的描述请参见文件头部”,但是一定要有这部分注释。
另外,公共接口的每个方法,都应该有注释来解释它的作用、参数、返回值以及其它影响。
为类的线程安全性作注释,如果有的话。如果类的实例可以被多个线程访问,记得注释多线程条件下的使用规则。
3.2 实现部分的注释
【建议】使用 | 来引用注释中的变量名及符号名而不是使用引号。
这会避免二义性,尤其是当符号是一个常用词汇,这使用语句读起来很糟糕。例如,对于符号 count :
// Sometimes we need |count| to be less than zero.
或者当引用已经包含引号的符号:
// Remember to call |StringWithoutSpaces("foo bar baz")|
注:不过中文注释的话可以忽略该条
3.3 对象所有权
【建议】当与 Objective-C 最常规的作法不同时,尽量使指针的所有权模型尽量明确。
继承自 NSObject 的对象的实例变量指针,通常被假定是强引用关系(retained),某些情况下也可以注释为弱引用(weak)或使用 __weak 生命周期限定符。同样,声明的属性如果没有被类 retained,必须指定是弱引用或赋予 @property 属性。然而,Mac 软件中标记上 IBOutlets 的实例变量,被认为是不会被类 retained 的。
当实例变量指向 CoreFoundation、C++ 或者其它非 Objective-C 对象时,不论指针是否会被 retained,都需要使用 __strong 和 __weak 类型修饰符明确指明。CoreFoundation 和其它非 Objective-C 对象指针需要显式的内存管理,即便使用了自动引用计数或垃圾回收机制。当不允许使用 __weak 类型修饰符(比如,使用 clang 编译时的 C++ 成员变量),应使用注释替代说明。
强引用及弱引用声明的例子:
@interface MyDelegate : NSObject {
@private IBOutlet NSButton *okButton_; // normal NSControl; implicitly weak on Mac only
AnObjcObject* doohickey_; // my doohickey
__weak MyObjcParent *parent_; // so we can send msgs back (owns me) // non-NSObject pointers...
__strong CWackyCPPClass *wacky_; // some cross-platform object
__strong CFDictionaryRef *dict_;
}
@property(strong, nonatomic) NSString *doohickey;
@property(weak, nonatomic) NSString *parent;
@end
3.4 方法声明注释
【建议】声明的方法,要注释方法功能,参数意义,返回值,格式如下:
//含参数,有返回值的方法注释
/**
* 初始化方法-2
* @param frame frame
* @param array 标题数组
* @param index 默认选中标题索引
* @return WJSegmentView
*/
- (instancetype)initWithFrame:(CGRect)frame
titleArray:(NSArray *)array
sellectIndex:(long)index;
//无参,无返回值方法注释
/**移除键盘*/
+ (void)removeKeyBoard{
NSArray *windows = [[UIApplication sharedApplication] windows];
for(UIWindow *window in windows){
[CommonMethods textViewTextFieldResignFirstResponder:window];
}
}
3.5 属性的注释
【建议】在声明文件中声明的实例变量在上一行加注释。或者直接在实例变量后边注释,与项目已有风格保持一致。
格式如下:
@property (nonatomic,copy)NSString *loginID;//登录ID
@property (nonatomic,copy)NSString *userName;//用户名
@property (nonatomic,copy)NSString *realName;//真是姓名
@property (nonatomic,copy)NSString *shareUrl;//二维码分享链接
@property (nonatomic,copy)NSString *appShareUrl;//社交分享链接
@property (nonatomic,copy)NSString *headPortraitAddr;//用户头像
/**标题数组*/
@property (nonatomic,retain)NSArray *titleArray;
/**选中按钮底部背景*/
@property (nonatomic,retain)UIView *backgroundView;
/**默认选中索引*/
@property (nonatomic,assign)long selectIndex;
4 代码组织
4.1 条件语句
- 当使用条件语句编码时,左手边的代码应该是"golden" 或 "happy"路径。也就是不要嵌套if语句,多个返回语句也是OK。
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
//Do something important
}
- 【建议】出现超过两层循环的代码,用函数或block替代
// 为了简化示例,没有错误处理,并使用了伪代码
// 糟糕的例子
- (Task *)creatTaskWithPath:(NSString *)path {
Task *aTask;
if ([path isURL]) {
if ([fileManager isWritableFileAtPath:path]) {
if (![taskManager hasTaskWithPath:path]) {
aTask = [[Task alloc] initWithPath:path];
}
else {
return nil;
}
}
else {
return nil;
}
}
else {
return nil;
}
return aTask;
}
// 改写的例子
- (Task *)creatTaskWithPath:(NSString *)path {
if (![path isURL]) {
return nil;
}
if (![fileManager isWritableFileAtPath:path]) {
return nil;
}
if ([taskManager hasTaskWithPath:path]) {
return nil;
}
Task *aTask = [[Task alloc] initWithPath:path];
return aTask;
}
4.2 实现文件目录结构
【建议】 在函数分组和protocol/delegate实现中使用#pragma mark -来分类方法,要遵循以下一般结构:
#pragma mark - Lifecycle
- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
#pragma mark - Custom Accessors
- (void)setCustomProperty:(id)value {}
- (id)customProperty {}
#pragma mark - IBActions
- (IBAction)submitData:(id)sender {}
#pragma mark - Public
- (void)publicMethod {}
#pragma mark - Private
- (void)privateMethod {}
#pragma mark - Protocol conformance
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}
#pragma mark - NSObject
- (NSString *)description {}