引言
一直以来都是在谈如何开发, 如开发的小技巧小经验
今天为什么突然说起编程规范来了呢?
因为在我看来, 编程规范是反映一个开发者或是一个开发团队素质和成熟度的最重要标志!
注意这里的措辞很严厉哈, 我用了"最重要"
当我们看到各种文不表意的函数命名、剪不断理还乱的文件和代码结构、以及不符合Apple官方基本规范的风格时
我想说: 程序员的加班 -- 你懂得(开发者制造的问题往往比他们解决的问题要多)
如果读者觉得有比编程规范更重要的事情, 可以来辩, 欢迎拍砖~
本文的代码规范基于The official raywenderlich.com Objective-C style guide, 但不局限于此, 结合实际开发遇到的问题, 做了部分修订和补充
背景
Apple官方的代码规范, 供补充参考:
- The Objective-C Programming Language
- Cocoa Fundamentals Guide
- Coding Guidelines for Cocoa
- iOS App Programming Guide
目录
- 语言
- 代码组织
- 空格
- 注释
- 命名
- 方法
- 变量
- 属性
- 点语法
- Object Literals
- 常量
- 枚举
- Case表达式
- 私有成员
- 布尔
- 条件语句
- 初始化方法
- 类构造方法
- CGRect函数
- Golden Path
- 错误处理
- 单例
- 换行
- 头文件
- 版权声明
- Xcode Project
语言
使用美式英语
Preferred:
UIColor *myColor = [UIColor whiteColor];
Not Preferred:
UIColor *myColour = [UIColor whiteColor];
代码组织
使用#pragma mark -
对function/protocol/delegate进行分组
#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
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}
#pragma mark - NSObject
- (NSString *)description {}
空格
条件表达式的括号 (如: if
/else
/switch
/while
), 左括号和表达式在同一行, 右括号另起一行
Preferred:
if (user.isHappy) {
// Do something
} else {
// Do something else
}
Not Preferred:
if (user.isHappy)
{
// Do something
}
else {
// Do something else
}
避免使用冒号对齐方式, 除非代码过长需要换行, 但是不要让block也同样保持冒号对齐
Preferred:
// blocks are easily readable
[UIView animateWithDuration:1.0f animations:^{
// something
} completion:^(BOOL finished) {
// something
}];
Not Preferred:
// colon-aligning makes the block indentation hard to read
[UIView animateWithDuration:1.0
animations:^{
// something
}
completion:^(BOOL finished) {
// something
}];
- 签代理
Preferred:
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer>
@property (nonatomic, assign) id<ChooseProvinceViewControllerdDelegate> delegate;
Not Preferred:
@interface UIViewController : UIResponder<NSCoding,UIAppearanceContainer>
@property (nonatomic, assign) id <ChooseProvinceViewControllerdDelegate> delegate;
注释
确实需要, 才加注释
注释是用来解释why, 而不是what
Preferred:
// alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
Not Preferred:
/**
* show head image
*/
@property (nonatomic, strong) DDImageView *headView;
// im conversation done edit
- (void)imConversationDoneEdit;
- 添加的注释要保持更新或删除
命名
- 使用长且描述性强的命名方式
Preferred:
UIButton *settingsButton;
Not Preferred:
UIButton *setBut;
- 在类名和常量名前加上3个字母的前缀(Core Data entity names去掉前缀)
需要统一整个工程的命名前缀, 因为OC里没有namespace, 工程越庞大后果越严重
- 常量的命名采用"驼峰"的命名方式
Preferred:
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3f;
Not Preferred:
static NSTimeInterval const fadetime = 1.7f;
成员的命名采用首字母小写的"驼峰"命名方式, 并且使用auto-synthesis方式声明成员, 而不是@synthesize方式
Preferred:
@property (nonatomic, copy) NSString *descriptiveVariableName;
Not Preferred:
id varnm;
命名时注意存储数据的数据结构发生改变的时候,会不会引起命名的改变。
Preferred:
@property (nonatomic, strong) NSArray *actionSheetItems;
@property(nullable, nonatomic, copy) NSArray<UIBarButtonItem *> *items;
Not Preferred:
@property (nonatomic, strong) NSArray *actionSheetItemArray;
图标资源命名
模块名+功能名+[状态],( [ ] : 表明可选)
图标在Xcode里面的名称需要与图标的物理命名保持一致
Preferred:
addressbook_isvnetwork // 指代没有状态的图标名
addressbook_call_normal
addressbook_call_highlight // 指代有多种状态的图标名
conference_member_delete_normal
conference_member_delete_highlight // 指代功能名较长且有多种状态的图标名
下划线
使用self.
方式来访问对象的成员(参考点语法), 从视觉上就可以区分出哪些是本对象成员
注意:
在初始化方法(
init
,initWithCoder:
等),dealloc
以及setters/getters方法中中必须使用保留的_variableName局部变量不要用下划线开头! 因为下划线开头的命名方式是Apple保留的命名方式
方法
方法类型(-/+)的后面有个空格
每一个参数都要有描述
注意:
- 不要在多个参数的描述中添加"and", 例如下面的
initWithWidth:height:
方法
Preferred:
- (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;
Not Preferred:
-(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
避免直接使用_viriableName方式访问对象的成员, 除了在初始化方法(init
, initWithCoder:
等), dealloc
方法以及setters/getters方法中点语法
了解更多关于在初始化和dealloc中使用accessor方法, 请参考这里.
Preferred:
@interface RWTTutorial : NSObject
@property (nonatomic, copy) NSString *tutorialName;
@property (nonatomic, copy) NSArray<UIBarButtonItem *> *items;
@end
Not Preferred:
@interface RWTTutorial : NSObject {
NSString *tutorialName;
}
如果变量是一个度量的话(如按时间长度或者字节数),那么最好把名字带上它的单位
Preferred:
NSString *durationOfSeconds;
Not Preferred:
NSString *duration;
属性
属性要按照先atomicity后storage的顺序, 这是为了和Apple的Interface Builder生成的代码保持一致.不用对齐,根据功能用空格实现分组
Preferred:
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nullable, readonly, nonatomic, copy) NSString *tutorialName;
Not Preferred:
@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (nonatomic) NSString *tutorialName;
注意:
- 控件的storage属性通常设置为weak, 而非strong
如果成员引用的对象是可变对象, 那么需要使用copy
而非strong
Why?
因为即使成员的类型是NSString
, 但实际传入的如果是NSMutableString
的对象
该对象在你不知情的情况下, 仍然会被修改
Preferred:
@property (nonatomic, copy) NSString *tutorialName;
Not Preferred:
@property (nonatomic, strong) NSString *tutorialName;
点语法
点语法实际是对accessor方法的封装
了解更多点语法, 请参考这里
访问和操作对象成员, 推荐使用点语法; Bracket notation is preferred in all other instances.
Preferred:
NSInteger arrayCount = [self.array count];
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;
Not Preferred:
NSInteger arrayCount = self.array.count;
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;
Object Literals
使用Object Literals(@
)方式来快速创建NSString
, NSDictionary
, NSArray
, NSNumber
实例
注意:
- 如果传给
NSArray
,NSDictionary
的值是nil会导致crash
Preferred:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingStreetNumber = @10018;
Not Preferred:
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];
- 容器类型最好标明存储数据的类型
Preferred:
@property (nonatomic, readonly, nullable) NSArray<NSIndexPath *> *indexPathsForVisibleRows;
- (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView ;
- 在数组和字典中, 使用索引和关键字来获取数据
Preferred:
NSString *name = names[1];
NSString *product = [productManagers valueForKey:@"kate"]
Not Preferred:
NSString *name = [names objectAtIndex:1];
NSString *product = [productManagers objectForKey:@"@kate"]
常量
使用static
而非#define
来声明常量; unless explicitly being used as a macro
用const修饰时,const右边的总是不能被修改.声明时不用对齐,根据功能用空格实现分组
Preferred:
static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";
static CGFloat const RWTImageThumbnailHeight = 50.0f;
Not Preferred:
#define CompanyName @"RayWenderlich.com"
#define thumbnailHeight 2.0f
- 宏定义
使用大写字母,用_分割单词,宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来
不到万不得已不推荐使用宏定义像数字常量,通知的参数一般不推荐使用宏定义,推荐使用static const 的形式
Preferred:
#define SCREEN_RECT ([UIScreen mainScreen].bounds)
#define SCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)
- 浮点数
使用浮点数在数值的后面加上f,用来区别double类型
Preferred:
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3f;
Not Preferred:
static NSTimeInterval const RWTTutorialViewControllerNavigationFadeAnimationDuration = 0.3;
枚举
使用NS_ENUM()
声明枚举, 因为它具有更强的类型检查机制
For Example:
typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
RWTLeftMenuTopItemMain,
RWTLeftMenuTopItemShows,
RWTLeftMenuTopItemSchedule
};
当然你还是可以显式地设置枚举的值
typedef NS_ENUM(NSInteger, RWTGlobalConstants) {
RWTPinSizeMin = 1,
RWTPinSizeMax = 5,
RWTPinCountMin = 100,
RWTPinCountMax = 500,
};
不要使用老式的枚举定义方式, 除非编写CoreFoundation C的代码
Not Preferred:
enum GlobalConstants {
kMaxPinSize = 5,
kMaxPinCount = 500,
};
Case表达式
Case表达式通常不需要加括号
Case内容有if判断句, for循环和局部变量时, 需要加上括号 (左括号的右边, break放在括号外). break与下一个case之间有空行.
switch (condition) {
case 1: {
for () {
// ...
}
}
break;
case 2: {
if {
//
}
}
break;
case 3:
// ...
break;
default:
// ...
break;
}
多个Case表达式执行相同的逻辑, 使用fall-through
方式(即删除表达式里的break)
此时, 需要加上注释说明注释
switch (condition) {
case 1:
// ** fall-through! **
case 2:
// code executed for values 1 and 2
break;
default:
// ...
break;
}
私有成员
私有属性要在类的实现中声明: 放在class extension(即匿名category)中
For Example:
@interface RWTDetailViewController ()
@property (nonatomic, strong) GADBannerView *googleAdView;
@property (nonatomic, strong) ADBannerView *iAdView;
@property (nonatomic, strong) UIWebView *adXWebView;
@end
布尔
Objective-C使用YES
和NO
, 而true
和false
只用在CoreFoundation, C或C++代码里
Since nil
resolves to NO
it is unnecessary to compare it in conditions. Never compare something directly to YES
, because YES
is defined to 1 and a BOOL
can be up to 8 bits.
This allows for more consistency across files and greater visual clarity.
Preferred:
if (someObject) {}
if (![anotherObject boolValue]) {}
Not Preferred:
if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this
if (isAwesome == true) {} // Never do this
如果BOOL
类型的属性是一个形容词, 那么可以去掉"is"前缀, 但get accessor中仍然需要保留前缀
@property (assign, getter=editable) BOOL editable;
条件语句
条件语句要加上括号, 即使是一行语句, 否则会存在隐患: even more dangerous defect
Preferred:
if (!error) {
return success;
}
Not Preferred:
if (!error)
return success;
or
if (!error) return success;
三元运算符
如果可以使代码变得简洁和高效, 那么可以考虑使用三元运算符?:
, 否则还是用if
表达式
通常, 给变量赋值时, 可以考虑使用三元运算符?:
Preferred:
NSInteger value = 5;
result = (value != 0) ? x : y;
BOOL isHorizontal = YES;
result = isHorizontal ? x : y;
Not Preferred:
result = a > b ? x = c > d ? c : d : y;
初始化方法
初始化方法要和Apple模板保持一致
- (instancetype)init {
self = [super init];
if (self) {
// Do something
}
return self;
}
返回值类型是'instancetype'而不是'id'
关于instancetype, 请参考类构造方法
类构造方法
类构造方法返回值类型是'instancetype'而不是'id', 这样可以帮忙编译器推导出返回值得类型
@interface Airplane
+ (instancetype)airplaneWithType:(RWTAirplaneType)type;
@end
了解更多关于instancetype: NSHipster.com.
CGRect函数
要获取CGRect
的x
, y
, width
, height
, 不要直接访问结构体的成员, 而应该使用CGGeometry functions
Apple官方对于CGGeometry
的解释:
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.
Preferred:
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.0f, 0.0f, width, height);
Not Preferred:
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 Path
如果把需要执行的逻辑比作"golden"或"happy" path
那么在判断条件不满足时, 直接return退出方法, 而不是判断条件满足时, 执行该Golden Path
Preferred:
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
// Do something important
}
Not Preferred:
- (void)someMethod {
if ([someOther boolValue]) {
// Do something important
}
}
错误处理
通过方法的返回值而不是返回的error引用, 来做错误处理的判断条件
Preferred:
NSError *error;
if (![self trySomethingWithError:&error]) {
// Handle Error
}
Not Preferred:
NSError *error;
[self trySomethingWithError:&error];
if (error) {
// Handle Error
}
单例
单例对象的实例化必须要确保线程安全
+ (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.
换行
头文件中的方法, 使用冒号对齐的方式
Preferred:
- (void)setViewWithHeadImageUrl:(NSString *)headImageUrl
name:(NSString *)name
phoneNumber:(NSString *)phoneNumber;
Not Preferred:
- (void)setViewWithHeadImageUrl:(NSString *)headImageUrl name:(NSString *)name phoneNumber:(NSString *)phoneNumber;
实现文件或方法调用时, 不用冒号对齐
Preferred:
[[MSDBHelper sharedInstance] updateCallRecordsWithNumber:number isPersonalContact:YES oldName:oldContact.displayName toNewName:@""];
Not Preferred:
[[MSDBHelper sharedInstance] updateCallRecordsWithNumber:number
isPersonalContact:YES
oldName:oldContact.displayName
toNewName:@""];
如果语句过长, 需要考虑代码的分解和优化
Preferred:
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[ctpManager connectToHost:kHostNameCloudPhoneNoport onPort:kCTPPort userInfo:userInfo;
[userInfo setObject:[AccountLoginModel sharedInstance].userInfo.mobilephone forKey:CTP_AUTHUSER_USER_NAME, nil];
Not Preferred:
[ctpManager connectToHost:kHostNameCloudPhoneNoport onPort:kCTPPort userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[AccountLoginModel sharedInstance].userInfo.mobilephone, CTP_AUTHUSER_USER_NAME, nil];
头文件
自定义头文件放在系统头文件的前面
按功能分开,加空格以示区分
按字母表排序
Preferred:
#import "DailModel"
#import "DialView.h"
#import "DialViewController.h"
#import <QYCTPManager/CTPManager.h>
Not Preferred:
#import <QYCTPManager/CTPManager.h>
#import "DialView.h"
#import "DailModel"
#import "DialViewController.h"
不要引入无关的头文件
尽量使用向前声明取代引入,这样不仅可以缩减编译时间,而且还能降低彼此依赖程度
版权声明
Preferred:
// MSDetailRecordsViewController.m
// CloudPhone
//
// Created by chenguang (guochenguang@qiyoukeji.com) on 15-12-6.
// Copyright (c) 2015年 QIYOU Ltd. All rights reserved.
//
Not Preferred:
// MSDetailRecordsViewController.m
// chenguang
//
// Created by chenguang on 15-12-6.
// Copyright (c) 2015年 CloudPhone. All rights reserved.
//
Xcode Project
Xcode Group要和文件系统里的文件夹关联起来
代码不仅要按照类型分组, 也要按照功能和特性进行分组
如果可以的话, 打开Build Settings里的"Treat Warnings as Errors"选项
并且尽可能多的打开additional warnings
如果需要忽略特定的warning, 请参考Clang's pragma feature.
其他Objective-C编程规范
- Robots & Pencils
- New York Times
- GitHub
- Adium
- Sam Soffes
- CocoaDevCentral
- Luke Redpath
- Marcus Zarra
更多文章, 请支持我的个人博客