首页 关于 微信公众号
欢迎关注我的微信公众号

Objective-C 编码风格指南

背景

本文主要是对以下几个编码规范的整理:

这里有些关于编码风格 Apple 官方文档,如果有些东西没有提及,可以在以下文档来查找更多细节:

语言

使用美式英语。别用拼音。

推荐:

UIColor *myColor = [UIColor whiteColor];

不推荐:

UIColor *myColour = [UIColor whiteColor];
UIColor *woDeYanSe = [UIColor whiteColor];

代码结构

使用 #pragma mark - 根据「代码功能类别」、「protocol/delegate 方法实现」等依据对代码进行分块组织。代码的组织顺序从整体上尽量遵循我们的认知顺序,组织规范如下:

// 先描述这个类是什么,它的属性有什么。
// 每个属性的 getter 方法在前,setter 方法在后。属性的 getter/setter 方法的顺序与属性声明顺序一致。
#pragma mark - Property

- (id)customProperty {}
- (void)setCustomProperty:(id)value {}

// 再描述这个类的生命周期,从出生到消亡。
// 按照生命周期的顺序来排序相关方法。
#pragma mark - Lifecycle

- (instancetype)init {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)viewDidAppear:(BOOL)animated {}
- (void)viewWillDisappear:(BOOL)animated {}
- (void)viewDidDisappear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
- (void)dealloc {}

// 如果这是一个 UIViewController 类,可以接着描述这个页面可以跳转到的其他页面。
#pragma mark - Navigation

- (void)goToMainPage {}
- (void)goToUserPage {}


// 接着描述这个类的响应方法,能做哪些交互。
// 比如:按钮点击的响应方法、手势的响应方法等等。
#pragma mark - Action

- (IBAction)submitData:(id)sender {}

// 然后描述这个类的其他分组方法。这里的分组可以是多个,如何分组可以由你扩展。
#pragma mark - <Other Functional Grouping>

- (void)someGroupedMethod {}


// 接下来描述这个类实现的 Protocol/Delegate 的方法。
// 先放自定义的 Protocol/Delegate 方法,后放官方提供的 Protocal/Delegate 方法。
#pragma mark - <Protocol/Delegate Conformance>
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate

// 然后是对继承的父类中方法重载。
// 先发自定义的父类方法重载,后方官方父类的方法重载。
#pragma mark - <Superclass Overridden>

- (void)someOverriddenMethod {}

#pragma mark - NSObject

- (NSString *)description {}

代码如流水一样,去叙述一个类。

空格

推荐:

if (user.isHappy) {
	// Do something
} else {
	// Do something else
}

不推荐:

if (user.isHappy)
{
	// Do something
}
else {
	// Do something else
}

推荐:

// blocks are easily readable
[UIView animateWithDuration:1.0 animations:^{
	// something
} completion:^(BOOL finished) {
	// something
}];

不推荐:

// colon-aligning makes the block indentation hard to read
[UIView animateWithDuration:1.0
                 animations:^{
					// something
                 }
                 completion:^(BOOL finished) {
					// something
                 }];

注释

当你写代码注释时,需要注意你的注释是解释为什么要有这段代码。一段注释要确保跟代码一致更新,否则就删掉。

一般避免使用块注释,这样占用空间太大,代码应该尽量做到自解释,代码即注释。当然,也有例外:你的注释是为了生成文档用。

命名

你可能是从 Java、Python、C++ 或是其他语言转过来的,但是来到 Objective-C 这地盘,请遵守苹果的命名规范,这样你才能使得自己的代码与周边和谐统一,尤其需要注意 memory management rules (NARC) 相关的命名规范.

长的、描述性的方法和变量命名是好的,这使得代码更容易被读懂。

推荐:

UIButton *settingsButton;

不推荐:

UIButton *setBut;

在类名和常量名上应该使用两个或三个字母的前缀(比如:CX、TB 等等)。但是在 Core Data 实体命名时应该省略前缀。

常量应该使用驼峰式命名规则,所有的单词首字母大写,并加上与类名有关的前缀。

推荐:

static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3;

不推荐:

static NSTimeInterval const fadetime = 1.7;

属性也是使用驼峰式命名规则,但首单词的首字母小写。对属性使用 auto-synthesis,而不是手动编写 @synthesize 语句,除非你有一个好的理由。

推荐:

@property (strong, nonatomic) NSString *descriptiveVariableName;

不推荐:

id varnm;

下划线

当使用属性时,用 self. 来访问,这就意味着所有的属性都很有辨识度,因为他们前面有 self.

但是有 2 个特列:

局部变量不要包含下划线。

方法

在方法签名中,应该在方法类型(-/+ 符号)之后有一个空格。在方法各段之间应该也有一个空格(符合 Apple 的风格)。在参数之前应该包含一个描述性的关键字来描述参数。

and 这个词的用法应该保留,它不应该用于多个参数之间。

推荐:

- (void)setExampleText:(NSString *)text image:(UIImage *)image;
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
- (id)viewWithTag:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

不推荐:

-(void)setT:(NSString *)text i:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)taggedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height;  // Never do this.

变量

变量尽量以描述性的方式来命名。除了在 for() 循环中,应该尽量避免单个字符的变量命名。

表示指针的星号应该和变量名在一起,比如:应该是 NSString *text,而不是 NSString* text 或者 NSString * text,除了一些特别的情况。

应该使用私有属性,而不要再使用实例变量了。这样可以保持代码的一致性。

除了在一些初始化方法(init, initWithCoder:, etc…)、销毁方法(dealloc)和自定义的 setters/getters 方法中外,不要直接使用下划线的方式访问实例变量。详情参见这里

推荐:

@interface RWTTutorial : NSObject

@property (strong, nonatomic) NSString *tutorialName;

@end

不推荐:

@interface RWTTutorial : NSObject {
	NSString *tutorialName;
}

属性特性

属性特性的顺序应该是:存储特性、访问特性、原子特性、getter/setter。其中存储特性、原子特性应该显式地列出来,有助于新手阅读代码。与在 Interface Builder 连接 UI 元素时自动生成代码一致。

推荐:

@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (strong, nonatomic) NSString *tutorialName;
@property (assign, readonly, nonatomic, getter=isFinished) BOOL finished;

不推荐:

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic) NSString *tutorialName;

具有值拷贝类型特定的属性(如:NSString)应该优先使用 copy 而不是 strong。这是因为即使你声明一个 NSString 类型的属性,有人也可能传入一个 NSMutableString 的实例,然后在你没有注意的情况下修改它。

推荐:

@property (copy, nonatomic) NSString *tutorialName;

不推荐:

@property (strong, nonatomic) NSString *tutorialName;

点符号语法

点符号语法是对方法调用语法很方便的一种封装。在返回属性时,使用点符号语法,属性的 getter/setter 方法也能确保被调用。更多信息阅读这里

我们应该总是使用点符号语法来访问或者修改属性,因为它使得代码更加简洁。[] 则应该用在其他场景下。

推荐:

NSInteger arrayCount = self.array.count; // `count` is a property of NSArray.
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate; // `sharedApplication` is not a property of UIApplication.

不推荐:

NSInteger arrayCount = [self.array count]; // `count` is a property of NSArray.
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate; // `sharedApplication` is not a property of UIApplication.

字面值

在创建 NSStringNSDictionaryNSArrayNSNumber 对象时,应该使用字面值语法。尤其需要注意创建 NSArrayNSDictionary 对象时,不能传入 nil,否则会造成 crash。

推荐:

NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone": @"Kate", @"iPad": @"Kamal", @"Mobile Web": @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingStreetNumber = @10018;

不推荐:

NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingStreetNumber = [NSNumber numberWithInteger:10018];

常量

比起硬编码字符串或数字的形式,我们应该常量来定义复用型变量,因为常量更容易被修改,而不需要我们 find + replace。使用常量时,我们应该使用 static 而不是 #define 一个类型不明的宏。

推荐:

static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";

static CGFloat const RWTImageThumbnailHeight = 50.0;

不推荐:

#define CompanyName @"RayWenderlich.com"

#define thumbnailHeight 2

枚举类型

当使用枚举时,我们要用 NS_ENUM() 而不是 enum

例如:

typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
	RWTLeftMenuTopItemMain,
	RWTLeftMenuTopItemShows,
	RWTLeftMenuTopItemSchedule
};

你可以显示的赋值:

typedef NS_ENUM(NSInteger, RWTGlobalConstants) {
	RWTPinSizeMin = 1,
	RWTPinSizeMax = 5,
	RWTPinCountMin = 100,
	RWTPinCountMax = 500,
};

不推荐:

enum GlobalConstants {
	kMaxPinSize = 5,
	kMaxPinCount = 500,
};

Case 语句

除非编译器强制要求,一般在 Case 语句中是不需要加括号的。当一个 Case 语句包含多行代码,应该加上括号。

switch (condition) {
	case 1:
		// ...
		break;
	case 2: {
		// ...
		// Multi-line example using braces
		break;
	}
	case 3:
		// ...
		break;
	default: 
		// ...
		break;
}

如果一段代码被多个 Case 语句共享执行,那就要用 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;
}

在 Swith 中使用枚举类型时,是不需要 default 语句的,例如:

RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;

switch (menuType) {
	case RWTLeftMenuTopItemMain:
		// ...
		break;
	case RWTLeftMenuTopItemShows:
		// ...
		break;
	case RWTLeftMenuTopItemSchedule:
		// ...
		break;
}

私有属性

私有属性应该在类的实现文件(xxx.m)中的匿名扩展(Anonymous Category)中声明。除非是要去扩展一个类,否则不要使用命名扩展(Named Category)。如果你要测试私有属性,你可以通过 <headerfile>+Private.h 的方式把私有属性暴露给测试人员。

For Example:

@interface RWTDetailViewController ()

@property (strong, nonatomic) GADBannerView *googleAdView;
@property (strong, nonatomic) ADBannerView *iAdView;
@property (strong, nonatomic) UIWebView *adXWebView;

@end

布尔值

Objective-C 使用 YESNO 作为 BOOL 值。因此,truefalse 只应该在 CoreFoundation、C、C++ 代码中使用。由于 nil 会被解析为 NO,所以没有必要在条件语句中去比较它。 另外,永远不要拿一个对象和 YES 比较,因为 YES 被定义为 1 并且 BOOL 值最多 8 bit。

这时为了在不同代码中保持一致性和简洁性。

推荐:

if (someObject) {}
if (![anotherObject boolValue]) {}

不推荐:

if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this.
if (isAwesome == true) {} // Never do this.

如果一个 BOOL 类型的属性是形容词,那么它的命名可以省略掉 “is” 前缀,但是我们还是需要给它指定惯用的 getter 方法名,例如:

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

更多内容详见:Cocoa Naming Guidelines

条件语句

条件语句应该使用大括号包围,即使能够不用时(比如条件代码只有一行)也不要省略大括号,这样可以最大可能的避免出错(比如条件语句不小心被注释了),同时也保持了大括号的使用风格一致。

推荐:

if (!error) {
	return success;
}

不推荐:

if (!error)
	return success;

或者

if (!error) return success;

三元操作符

只有在能提高代码清晰性和可读性的情况下,才应该使用三元操作符 ?:。单个条件判断时可以用到它,多个条件判断时还是用 if 来提高代码可读性吧。一般来说,使用三元操作符最好的场景是根据条件来赋值的时候。

非布尔类型的变量与某对象比较时最好加上括号来提高代码可读性,如果被比较的变量是布尔类型那就不用括号了。

推荐:

NSInteger value = 5;
result = (value != 0) ? x : y;

BOOL isHorizontal = YES;
result = isHorizontal ? x : y;

不推荐:

result = a > b ? x = c > d ? c : d : y;

初始化方法

Init 方法应该遵循 Apple 生成代码模板的命名规则。返回类型应该使用 instancetype 而不是 id

- (instancetype)init {
	self = [super init];
	if (self) {
		// ...
	}
	return self;
}

类构造方法

当使用类构造方法时,应该返回的类型是 instancetype 而不是 id。这样确保编译器正确地推断结果类型。

@interface Airplane
+ (instancetype)airplaneWithType:(RWTAirplaneType)type;
@end

查看更多关于 instancetype 的信息:NSHipster.com

CGRect 方法

当访问 CGRect 的 xywidthheight 属性时,总是使用 CGGeometry functions 相关的函数,而不是直接从结构体访问。

All functions described in this reference that take CGRect data structures as inputs implicitly standardize those rectangles before calculating their results. For this reason, your applications should avoid directly reading and writing the data stored in the CGRect data structure. Instead, use the functions described here to manipulate rectangles and to retrieve their characteristics.

推荐:

CGRect frame = self.view.frame;

CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);
CGRect frame = CGRectMake(0.0, 0.0, width, height);

不推荐:

CGRect frame = self.view.frame;

CGFloat x = frame.origin.x;
CGFloat y = frame.origin.y;
CGFloat width = frame.size.width;
CGFloat height = frame.size.height;
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };

黄金路径

当使用条件语句编写逻辑时,左手的代码应该是 “golden” 或 “happy” 路径。也就是说,不要嵌套多个 if 语句,即使写多个 return 语句也是 OK 的。

推荐:

- (void)someMethod {
	if (![someOther boolValue]) {
		return;
	}

	//Do something important
}

不推荐:

- (void)someMethod {
	if ([someOther boolValue]) {
		//Do something important
	}
}

单例

单例对象应该使用线程安全的方式来创建共享实例。

+ (instancetype)sharedInstance {
	static id sharedInstance = nil;

	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		sharedInstance = [[self alloc] init];
	});

	return sharedInstance;
}

这样会防止 possible and sometimes prolific crashes

换行符

换行符主要是在提高打印和网上阅读时的代码可读性时显得很重要。

例如:

self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];

一行较长的代码最好能换行再加一个 Tab。

self.productsRequest = [[SKProductsRequest alloc] 
  initWithProductIdentifiers:productIdentifiers];

Xcode 工程

物理文件应该与 Xcode 项目目录保持同步来避免文件管理杂乱。创建任何 Xcode group 应该与文件系统中的文件夹保持映射。代码分类除了以类型分类,从大的方面上也应该以功能分类。

如果可以的话,打开 Xcode 的 Treat Warnings as Errors 来降低对 warning 的容忍度。如果有时候确实要忽略某一个 warning,你可以使用 Clang’s pragma feature

Blog

Opinion

Project