目录
前言
好的代码有一些特性:简明,自我解释,优秀的组织,良好的文档,良好的命名,优秀的设计以及可以被久经考验。
原则一:代码应该简洁易懂,逻辑清晰
- 不要过分追求技巧,降低程序的可读性。
- 简洁的代码可以让bug无处藏身。要写出明显没有bug的代码,而不是没有明显bug的代码。
原则二:面向变化编程,而不是面向需求编程。
- 需求是暂时的,只有变化才是永恒的。
- 本次迭代不能仅仅为了本次的需求,需要写出扩展性强,易修改的代码。
原则三:先保证程序的正确性,防止过度工程
- 先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
- 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
- 先写出可用,简单,明显没有bug的代码,再考虑测试的问题。
在正确可用的代码写出之前就过度地考虑扩展,重用的问题,使得工程过度复杂。
命名规范
命名严禁使用拼音、数字与英文混合的方式,更不允许直接使用中文的方式。
命名使用正确的英文拼写和语法可以让阅读者易于理解,避免歧义。
命名含义清楚,尽量使用全称不使用缩写(除了公认的缩写)。
命名尽量做到自注释(通过名字不需要注释就能了解其功能和作用),若做不到就加相应注释。
命名主要方法:
- 大驼峰命名法:每个单词的首字母大写:NameTextField。
- 小驼峰命名法:第一个单词首字母小写,其余都大写:nameTextField。
- 前缀大驼峰命名法:在大驼峰命名法的基础上加项目前缀大写:ISTNameTextField。
- 全大写下划线命名法:所有字母全部大写,单词用下划线分割:UIKIT_EXTERN。
- k大驼峰命名法:在大驼峰命名法的基础上加k:kNameTextFieldCount。
- 下划线小驼峰命名法:在小驼峰命名法的基础上加下划线(_):_nameTextField。
注:命名方法的具体使用会在每个规范里指出。
注释规范
注释要么一直维护,要么干脆删掉。
块注释应该被避免,代码本身应该尽可能就像文档一样表示意图,只需要很少的打断注释。
优秀的代码大部分是可以自注释的,完全可以用代码本身来表达它到底在干什么,而不需要注释的辅助。
但并不是说一定不能写注释,有以下四种情况比较适合写注释:
-
公共接口(注释要告诉阅读代码的人,当前类的功能描述、传入的参数、返回的值、如何使用此类等)。
-
涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)。
-
容易产生歧义的代码(但是严格来说,容易让人产生歧义的代码是不允许存在的)。
-
记录一个复杂的类的完整的实现思想和步骤,写在类的创建注释里面(可以要让阅读代码的人更方便地理解和使用这个类)。
除了上述这四种情况,如果别人只能依靠注释才能读懂你的代码的时候,就要反思代码出现了什么问题。
#define规范
全局常量宏和类函数宏需要定义在一个特定的头文件里,供整个项目使用。
私有常量宏只能定义在特定的.m文件中。
本文仅是为了完善#define规范,所以还是介绍了常量宏的规范,常量宏的规范这部分可以跳过。。
千万不要用常量宏,不管是全局还是私有。而是用extern和const来声明全局常量,用static和const来声明私有常量。
全局常量宏
规范要求:
- 使用全大写下划线命名法,但是需要添加项目前缀:前缀_功能。
- 以常量数值进行对齐。
举个栗子:
#define IST_NAVIGATIONBAR_HEIGHT 64
#define IST_NAVIGATIONBAR_WIDTH 64
私有常量宏
规范要求:
- 使用k大驼峰命名法:k + 功能。
- 以常量数值进行对齐。
举个栗子:
#define kNavigationBarHeight 64
#define kNavigationBarWidht 64
类函数宏
规范要求:
- 使用前缀大驼峰命名法:前缀 + 功能。
- 以下面栗子方式进行对齐。
举个栗子:
#define ISTUserDefaults [NSUserDefaults standardUserDefaults]
#define ISTNotificationCenter [NSNotificationCenter defaultCenter]
#define IST_SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define IST_SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#pragma mark规范
pragma mark -
是在类内部模块组织函数方法的好办法。一定要使用
#pragma mark -
来分离:不同功能组的方法、protocols 的实现、对父类方法的重写等。
规范要求:
- #pragma mark - (此处可以写英文,但是英文不可缩写且意思要完整,也可以写中文,主要取决于哪种方式更简洁明了)
- #pragma mark - 上下和方法之间都要空一行。
举个栗子:
#pragma mark - life cycle
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(color:) name:@"Notification" object:nil];
// Do any additional setup after loading the view, typically from a nib.
}
#pragma mark - 通知响应事件
- (void)colorChange:(UIColor *)color {
self.view.backgroundColor = color;
}
#import规范
如果需要#import(或@class)两个文件(或类)以上,就要根据功能进行分组,并且对分组进行注释。
规范要求:
- 在.h文件中只能使用@class来声明类。
- 在.h文件中可以使用#import引用非类的配置头文件。
- 在.m文件中使用#import来引用类头文件。
- #import分组顺序从上到下是:系统框架、第三方库、自定义类、配置文件。
- @class分组顺序同#import。
举个栗子:
//系统框架
#import <Foundation/Foundation.h>
//第三方库
#import <MBProgressHUD.h>
//view
#import "IOSBookView.h"
//model
#import "IOSBookModel.h"
//viewModel
#import "IOSBookviewModel.h"
//viewController
#import "IOSBookViewController.h"
//其他(包括与当前.m对应.h和工具类、常量类、配置类等)
#import "IOSTool.h"
#import "IOSConstant.h"
类规范
在实际开发中,一般都会给项目中所有的类加上属于本项目的前缀。
使用大驼峰命名,一般格式:前缀 + 功能 + 类型后缀。
举个栗子:
ISTHomePageViewController
IST(项目简写)+ HomePage(功能) +ViewController(类型后缀)
其他例子:
ISTHomePageView
ISTHomePageModel
@Interface规范
常量规范
1)全局常量
全局常量应全部声明在同一个.h文件中,供整个项目使用,并在.m文件中实现全局常量,不暴露出来。
规范要求:
- 使用前缀大驼峰命名法:项目前缀 + 功能。
- 全局常量的声明应使用extern和const修饰。
- 多个全局常量以最远的 = 进行对齐。 不同数据类型可以分开对齐。
举个栗子:
//.h
extern NSString * const ISTNetworkStatusChangeNotification;
extern NSString * const ISTStatusChangeNotification;
//.m
NSString *const ISTNetworkStatusChangeNotification = @"ISTNetworkStatusChangeNotification";
NSString *const ISTNetworkChangeNotification = @"ISTNetworkChangeNotification";
2)私有常量
规范要求:
- 使用k大驼峰命名法:k + 功能。
- 私有常量的声明应使用static和const修饰。
- 多个私有常量以最远的 = 进行对齐。不同数据类型可以分开对齐。
举个栗子:
static const NSInteger kPasswordCount = 16;
static const NSInteger kNavigationBarHeight = 64;
static NSString * const kUserName = @"SunSatan";
static NSString * const kUserMessage = @"Message";
变量规范
1)实例变量
规范要求:
- 使用小驼峰命名法,一般格式:功能 + 类型后缀(BOOL类型命名必须以is开头可以不用类型后缀)。
- 实例变量都是使用时才定义,很分散,所以没有特别的对齐要求。
举个栗子:
UIButton *settingsButton;
NSArray *dateArray;
2)私有成员变量
规范要求:
- 使用下划线小驼峰命名法,一般格式:_ + 功能 + 类型后缀(BOOL类型命名必须以is开头可以不用类型后缀)。
- 可以根据类型来划分模块,且以
“*”
分类对齐(基础数据类型另外分类对齐)。
举个栗子:
@interface ViewController () {
NSString *_yearDataString;
NSString *_dayDataString;
BOOL _isAdsOpen;
BOOL _isHomeMenuOpen;
}
@property规范
永远不要在 init 方法和dealloc
方法里面用 getter 和 setter 方法(点语法访问属性),应当直接访问@property的实例变量。
在上一条的描述情况以外,其他任何地方都应当使用 getter 和 setter 方法(点语法访问属性)。
在实现文件中应避免使用@synthesize,
因为Xcode已经自动添加了。
公共属性应该定义在.h文件中,私有属性应该定义在.m文件的类扩展中。
规范要求:
- 使用小驼峰命名,一般格式:功能 + 类型后缀(BOOL类型命名必须以is开头可以不用类型后缀)。
- 属性的关键字推荐按照(原子性,读写,内存管理)顺序排列。
- 根据内存管理关键字或数据类型来划分模块,且以
“*”
分类对齐(基础数据类型另外分类对齐)。 - 所有注释应该一起对齐。
举个栗子:
@property (nonatomic, readwrite, strong) UILabel *tipsLabel; //这里写注释
@property (nonatomic, readwrite, strong) UIImageView *backgroundImageView; //这里写注释
@property (nonatomic, readwrite, copy) NSString *dayDataString; //这里写注释
@property (nonatomic, readwrite, copy) NSString *userNameString; //这里写注释
@property (nonatomic, readwrite, assign) int messageQuantityInt; //这里写注释
@property (nonatomic, readwrite, assign) float orderPaymentAmountFloat; //这里写注释
若要使@property有一个公共的 getter 和一个私有的 setter,应该在.h声明外部可访问的属性的关键字为 readonly
,并且在类扩展中重新定义该属性的关键字为 readwrite
。 这样外部无法修改该属性,而内部可以,从而确保了@property访问安全性。
举个栗子:
// .h文件中
@interface MyClass : NSObject
@property (nonatomic, readonly, strong) NSObject *object;
@end
// .m文件中
@interface MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object;
@end
@implementation MyClass
// Do Something cool
@end
描述BOOL
属性的词如果是形容词,那么setter不应该带is
前缀,但它对应的 getter 访问器应该带上这个前缀。
举个栗子:
@property (assign, getter=isEditable) BOOL editable;
@implementation规范
方法规范
不能使用 "and" 这个词来阐明方法有多个参数。
一个方法的长度必须限制在50行以内。
一个方法只做一件事,只有一个具体的功能(单一原则)。
对于有返回值的方法,每一个分支都必须有返回值。
方法一开始就要对参数的正确性和有效性进行检查,参数错误立即返回,参数完全无误后才能开始do some important。
规范要求:
- 使用小驼峰命名法。
- 方法名与方法类型符号(- / +)之间应该以空格间隔。
- 方法名与第一个参数使用 "With" 连接。
- 方法每个参数前应该总是有一个描述性的关键词。
- 方法与方法之间应该有一个空行,多个参数之间应有一个空格。
- 方法实现内部应空一行再写代码。
- 方法内的空行应该用来分离模块,但是通常不同的功能应该用新的方法来定义。
- 当方法的参数大于四个时,应该总是让冒号对齐,可以让代码更具有可读性。
- 如果使用冒号对齐时,方法参数中有block,那么应该把 block 定义为变量,或者重新考虑代码签名设计。
- 在.h中使用 command+option+/ 快捷键给公共方法添加必要的注释。
举个栗子:
//声明
- (void)setExampleText:(NSString *)text image:(UIImage *)image;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
//实现
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height {
//这里空一行
//code
}
//注释
/**
<#Description#>
@param store <#store description#>
@param searchService <#searchService description#>
@return <#return value description#>
*/
- (instancetype)initWithOperationsStore:(id<ZOCGenericStoreProtocol>)store searchService:(id<ZOCGenericSearchServiceProtocol>)searchService;
Initializer和dealloc规范
规范要求:
- 将
dealloc
方法放在实现文件的最前面(即使dealloc
方法什么也不做),init
应该跟在dealloc
方法后面。 - 如果有多个
init
方法, 指定init
方法应该放在最前面,间接init
方法跟在后面,这样更有逻辑性。 - 通常,在
init
方法中做的事情需要在dealloc
方法中撤销。 - 永远不要在 init 方法
和dealloc
方法里面用 getter 和 setter 方法(点语法访问属性),应当直接访问实例变量。
举个栗子:
- (instancetype)init {
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
Designated 和 Secondary 初始化方法
Objective-C 有指定初始化方法(designated initializer)和间接初始化方法(secondary initializer)的观念。
designated 初始化方法是提供所有的参数的初始化,而secondary 初始化方法是一个或多个参数的选择性初始化,并且提供一个或者更多的默认参数来调用 designated 初始化的初始化方法。
这个栗子很好地展示了Designated 和 Secondary 初始化方法的使用和它们之间的区别:
@implementation ZOCEvent
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date
location:(CLLocation *)location {
self = [super init];
if (self) {
_title = title;
_date = date;
_location = location;
}
return self;
}
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date {
return [self initWithTitle:title date:date location:nil];
}
- (instancetype)initWithTitle:(NSString *)title {
return [self initWithTitle:title date:[NSDate date] location:nil];
}
@end
initWithTitle:date:location:
就是 designated 初始化方法,另外的两个是 secondary 初始化方法。因为它们仅仅是 designated 初始化方法
类簇规范
使用信息进行(类的)初始化处理期间,会使用一个抽象类(通常作为初始化方法的参数或者判定环境的可用性参数)来完成特定的逻辑或者实例化一个该抽象类的具体的子类。
使用类簇可以帮助移除很多条件语句。
这个栗子很好地展示了如何创建一个类簇:
@implementation ZOCKintsugiPhotoViewController
- (id)initWithPhotos:(NSArray *)photos {
if ([self isMemberOfClass:ZOCKintsugiPhotoViewController.class]) {
self = nil;
if ([UIDevice isPad]) {
self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];
}
else {
self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
}
return self;
}
return [super initWithNibName:nil bundle:nil];
}
@end
注:
- 使用
[self isMemberOfClass:ZOCKintsugiPhotoViewController.class]
防止子类中重写初始化方法,避免无限递归。 self = nil
的目的是移除ZOCKintsugiPhotoViewController
实例上的所有引用。
懒加载(Lazy Loading)规范
当实例化一个对象需要耗费很多资源,或者配置一次就要调用很多配置相关的方法而你又不想弄乱这些方法时,就需要重写 getter 方法以延迟实例化(在调用getter 方法时才实例化),而不是在 init 方法里给所有对象都分配内存。
但是不要滥用懒加载,需要慎重考虑是否懒加载,因为懒加载同样存在一些问题。
- (NSDateFormatter *)dateFormatter {
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[_dateFormatter setLocale:enUSPOSIXLocale];
[_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"]; // 毫秒是SSS,而非SSSSS
}
return _dateFormatter;
}
条件规范
if-else
规范
条件语句体应该总是被大括号包围。
尽管有时候可以不使用大括号(比如条件语句体只有一行内容),但是这样做会带来很多问题隐患。
规范要求:
- { 紧跟在
if-else
这一行,} 则另起一行。 else需要要另起一行
。if-else
须穷举所有的情况,而且每个分支都须给出明确的结果。- 要使用return来提前返回错误情况,把最正确的情况放到最后返回。
if-else
超过五层的时候, 就要考虑重构, 多层的if-else
结构很难维护。if中的判断
条件过多过长的时候,应该一一换行将所有条件对齐。
举个栗子:
if (age < 0) {
return error;
}
else if (age > 200) {
return error;
}
return success;
黄金路径法则
在使用条件语句编程时,代码的左边距应该是一条“黄金”或者“快乐”的大道。
也就是说,不要嵌套 if
语句。使用多个 return 可以避免增加循环的复杂度,并提高代码的可读性。因为方法的重要部分没有嵌套在分支里面,并且可以很清楚地找到相关的代码。
举个栗子:
- (void)someMethod {
if (![someThing boolValue]) {
return;
}
if (![otherThing boolValue]) {
return;
}
// Do something important
}
//错误做法
- (void)someMethod {
if ([someOther boolValue]) {
// Do something important
}
}
复杂的表达式
当你的 if 子句有很多复杂的判断条件的时候,就应该把它们这些复杂的条件提取出来赋给一个 BOOL 变量,这样可以让逻辑更清楚,而且让每个子句的意义体现出来。
举个栗子:
BOOL nameContainsSwift = [sessionName containsString:@"Swift"];
BOOL isCurrentYear = [sessionDateCompontents year] == 2014;
BOOL isSwiftSession = nameContainsSwift && isCurrentYear;
if (isSwiftSession) {
// Do something very cool
}
三元运算符
三元运算符应该只用在它能让代码更加简洁和清楚的地方,而不是使用之后反而会使代码更难以理解。
举个栗子:
result = a > b ? x : y;
result = object ? : [self createObject];
//错误使用
result = a > b ? x = c > d ? c : d : y;
result = object ? object : [self createObject];
枚举规范
当使用枚举的时候,要使用Objective-C的基础类型定义,因为它有更强大的类型检查和代码补全。
并且是使用NS_ENUM和NS_OPTIONS定义枚举。
typedef NS_ENUM(NSUInteger, ZOCMachineState) {
ZOCMachineStateNone,
ZOCMachineStateIdle,
ZOCMachineStateRunning,
ZOCMachineStatePaused
};
typedef NS_OPTIONS(NSUInteger,NYTAdCategory){
NYTAdCategoryAutos = 1 << 0,
NYTAdCategoryJobs = 1 << 1,
NYTAdCategoryRealState = 1 << 2,
NYTAdCategoryTechnology = 1 << 3
};
Block规范
1)作为参数
当Block作为参数时,尽量只使用一个单独的 block 作为接口的最后一个参数。
把需要提供的数据和错误信息整合到一个单独 block 中,比分别提供成功和失败的 block 要好。
完成处理的 block 的参数很常见:第一个参数是调用者希望获取的数据,第二个是错误相关的信息。但需要遵循以下两点:
- 若
objects
不为 nil,则error
必须为 nil - 若
objects
为 nil,则error
必须不为 nil
- (void)downloadObjectsAtPath:(NSString *)path
completion:(void(^)(NSArray *objects, NSError *error))completion;
2)作为属性
3)循环引用
总是如下避免循环引用:
__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomethingWithData:data];
[strongSelf doSomethingWithData:data];
}
}];
NSNotification规范
把通知的名字定义为一个字符串常量,在公开的接口文件中将其声明为 extern
的, 并且在对应的实现文件里面定义。
通知的名字是在全局常量的命名法的基础上添加 Did/Will 动词和"Notifications" 后缀。
多个字符串常量的对齐方法与之前的全局常量一致。
举个栗子:
// .h
extern NSString * const ZOCFooDidBecomeBarNotification
// .m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";
Categories规范
规范要求:
- category名使用前缀大驼峰命名法:ISTOneLog。
- category中定义的方法名使用小写前缀下划线以及小驼峰命名法:ist_timeAgoShort。
举个栗子:
@interface UIView (ZOCOneLog)
+(void) zoc_viewLog;
@end
Protocols和delegate规范
Protocols
Protocols名使用类名+ 协议后缀(Delegate/DataSources/Protocols等)。
@required和@optional不可省略(哪怕其中没有声明方法),且@required在@optional之前。
Protocols方法必须以该类作为第一个参数,用于区分调用Protocols方法的多个该类实例对象。
@class delegateView;
@protocol delegateViewDelegate <NSObject>
@required
- (BOOL)start:(delegateView *)view;
@optional
- (void)colorChange:(delegateView *)view color:(UIColor *)color;
@end
//下面这种写法人神共怒
- (void)changeColorWithColor:(UIColor *)color;
delegate
delegate应与Protocols名中协议后缀一致,并且为全小写。
delegate属性应使用weak修饰和id数据类型。
@property(nonatomic, readwrite, weak) id <delegateViewDelegate> delegate;
对于@optional中的方法,委托者必须在发送消息前检查代理是否确实实现了特定的方法(否则会 crash),确保安全。
if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
[self.delegate signUpViewControllerDidPressSignUpButton:self];
}
单例模式规范
如果可能,请尽量避免使用单例模式而是使用依赖注入。 然而,如果一定要用单例模,请使用线程安全的模式来创建共享的单例。
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
资源包规范
Bundle Identifier规范
Bundle Identifier使用反域名命名法,全部采用小写字母,一般格式:域名后缀 + 公司顶级域名 + 应用名。
举个栗子:
com.companyname.applicationname
参考文献
注:为了写出这一篇属于自己的iOS-Objective-C编程规范,我参考了以下文章、文献:
《禅与 Objective-C 编程艺术(Zen and the Art of the Objective-C Craftsmanship 中文翻译)》
https://github.com/NYTimes/objective-c-style-guide
https://www.jianshu.com/p/21f059f04181
https://www.jianshu.com/p/9dd18e69a954
https://www.jianshu.com/p/1784cd67e8de
https://blog.csdn.net/qq350116542/article/details/51195386#commentBox