前言
本文是参考自己的部分代码规范和网上众多其它小伙伴的代码规范,整理和写出的。
目的
为了利于项目维护以及规范开发,促进成员之间Code Review的效率、为了优美的代码、为了成员的和谐,故提出以下开发规范,如有更好的建议,欢迎提出。
本文档的预期读者包括:iOS开发人员。
开始了,很长哦
这篇规范⼀共分为三个部分:
1、核⼼原则:介绍了这篇代码规范所遵循的核⼼原则。
2、通⽤规范:不局限于iOS的通⽤性的代码规范(使⽤C语⾔和Swift语⾔)。
3、iOS规范:仅适⽤于iOS的代码规范(使⽤Objective-C语⾔)。
⼀、核⼼原则
原则⼀:代码应该简洁易懂,逻辑清晰
因为软件是需要⼈来维护的。这个⼈在未来很可能不是你。所以⾸先是为⼈编写程序,其次才是计算机:不要过分追求技巧,降低程序的可读性。简洁的代码可以让bug⽆处藏身。要写出明显没有bug的代码,⽽不是没有明显bug的代码。
原则⼆:⾯向变化编程,⽽不是⾯向需求编程。
需求是暂时的,只有变化才是永恒的。本次迭代不能仅仅为了当前的需求,写出扩展性强,易修改的程序才是负责任的做法。
原则三:先保证程序的正确性,防⽌过度⼯程
过度⼯程(over-engineering):在正确可⽤的代码写之前就过度地考虑扩展,重⽤的问题,使⼯程过度复杂。
1、先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
2、先写出可⽤的代码,反复推敲,再考虑是否需要重⽤的问题。
3、先写出可⽤,简单,明显没有bug的代码,再考虑测试的问题。
⼆.通⽤规范
关于⼤括号
控制语句(if,for,while,switch)中,⼤括号开始与⾏尾
推荐这样写:
white(someCondition) {
}
//函数
void function(param1, param2) {
}
运算符
1.运算符与变量之间的间隔
1.1⼀元运算符与变量之间没有空格:
1.2⼆元运算符与变量之间必须有空格
!bValue
++iCount
fHeight = fWidth + fLength;
for(int i = 0; i < 10; i++)
变量
1.⼀个变量有且只有⼀个功能,尽量不要把⼀个变量⽤作多种⽤途
2.变量在使⽤前应初始化,防⽌未初始化的变量被引⽤
3.局部变量应该尽量接近使⽤它的地⽅
推荐这样写:
func someFunction() {
let index = ...;
//Do something With index
...
...
let count = ...;
//Do something With count
}
不推荐这样写:
func someFunction() {
let index = ...;
let count = ...;
//Do something With index
...
...
//Do something With count
}
if语句
1.不要使⽤过多的分⽀,要善于使⽤return来提前返回
推荐这样写:
- (void)someMethod {
if (!goodCondition) {
return;
}
//Do something
}
不推荐这样写:
- (void)someMethod {
if (goodCondition) {
//Do something
}
}
错误的情况
⽐较典型的例⼦我在JSONModel⾥遇到过:
-(id)initWithDictionary:(NSDictionary*)dict error:(NSError)err{
//⽅法1.参数为nil
if (!dict) {
if (err) *err =[JSONModelError errorInputIsNil];
return nil;
}
//⽅法2.参数不是nil,但也不是字典if (![dict isKindOfClass:[NSDictionary class]]) {
if (err) *err =[JSONModelError errorInvalidDataWithMessage:@"Attempt
to initialize JSONModel object using initWithDictionary:error: but the
dictionary parameter was not an 'NSDictionary'."];
return nil;
}
//⽅法3.初始化
self =[self init];
if (!self) {
//初始化失败
if (err) *err =[JSONModelError errorModelIsInvalid];
return nil;
}
//⽅法4.检查⽤户定义的模型⾥的属性集合是否⼤于传⼊的字典⾥的key集合(如果⼤于,则返回NO)
if (![self __doesDictionary:dict
matchModelWithKeyMapper:self.__keyMapper error:err]) {
return nil;
}
//⽅法5.核⼼⽅法:字典的key与模型的属性的映射
if (![self __importDictionary:dict withKeyMapper:self.__keyMapper
validation:YES error:err]) {
return nil;
}
//⽅法6.可以重写[self validate:err]⽅法并返回NO,让⽤户⾃定义错误并阻拦model的返回
if (![self validate:err]) {
return nil;
}
//⽅法7.终于通过了!成功返回model
return self;
}
可以看到,在这⾥,⾸先判断出各种错误的情况然后提前返回,把最正确的情况
放到最后返回。
2.条件表达式如果很⻓,则需要将他们提取出来赋给⼀个BOOL值
推荐这样写:
let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) {
// Do something
}
不推荐这样写:
if ( sessionName.hasPrefix("Swift") && (sessionDateCompontents.year ==
2014) ) {
// Do something
}
3.条件语句的判断应该是变量在左,常量在右
推荐这样写:
if (count == 6) {
}
或者
if (object == nil) {
}
或者
if (!object) {
}
4.每个分⽀的实现代码都必须被⼤括号包围
Switch语句
1.每个分⽀都必须⽤⼤括号括起来
//推荐这样写:
switch (integer) {
case 1: {
// ...
}
break;
case 2: {
// ...
break;
}
default:{
// ...
break;
}
}
2.使⽤枚举类型时,不能有default分⽀, 除了使⽤枚举类型以外,都必须有default分⽀
在Switch语句使⽤枚举类型的时候,如果使⽤了default分⽀,在将来就⽆法通过编译器来检查新增的枚举类型了。
函数
1.⼀个函数的⻓度必须限制在50⾏以内
通常来说,在阅读⼀个函数的时候,如果视需要跨过很⻓的垂直距离会⾮常影响代码的阅读体验。如果需要来回滚动眼球或代码才能看全⼀个⽅法,就会很影响思维的连贯性,对阅读代码的速度造成⽐较⼤的影响。最好的情况是在不滚动眼球或代码的情况下⼀眼就能将该⽅法的全部代码映⼊眼帘。
2.⼀个函数只做⼀件事(单⼀原则)
每个函数的职责都应该划分的很明确(就像类⼀样)。
// 推荐这样写:
dataConfiguration()
viewConfiguration()
// 不推荐这样写:
void dataConfiguration() {
...
viewConfiguration()
}
3.对于有返回值的函数(⽅法),每⼀个分⽀都必须有返回值
// 推荐这样写:
int function() {
if(condition1){
return count1
}else if(condition2){
return count2
}else{
return defaultCount
}
}
4.对输⼊参数的正确性和有效性进⾏检查,参数错误⽴即返回
// 推荐这样写:
void function(param1,param2) {
if(param1 is unavailable){
return;
}
if(param2 is unavailable){
return;
}
//Do some right thing
}
5.如果在不同的函数内部有相同的功能,应该把相同的功能抽取出来单独作为另⼀个函数
// 原来的调⽤:
void logic() {
a();
b();
if (logic1 condition) {
c();
} else {
d();
}
}
// 将a,b函数抽取出来作为单独的函数
void basicConfig() {
a();
b();
}
void logic1() {
basicConfig();
c();
}
void logic2() {
basicConfig();
d();
}
6.将函数内部⽐较复杂的逻辑提取出来作为单独的函数
⼀个函数内的不清晰(逻辑判断⽐较多,⾏数较多)的那⽚代码,往往可以被提取出去,构成⼀个新的函数,然后在原来的地⽅调⽤它这样你就可以使⽤有意义的函数名来代替注释,增加程序的可读性。
举⼀个发送邮件的例⼦:
openEmailSite();
login();
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
send();
// 中间的部分稍微⻓⼀些,我们可以将它们提取出来:
void writeEmail(title, content,receiver,attachment)
{
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
}
// 然后再看⼀下原来的代码:
openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();
7.避免使⽤全局变量,类成员(class member)来传递信息,尽量使⽤局部变量和参数。
在⼀个类⾥⾯,经常会有传递某些变量的情况。⽽如果需要传递的变量是某个全局变量或者属性的时候,有些朋友不喜欢将它们作为参数⽽是在⽅法内部就直接访问了:
class A {
var x;
func updateX() {
...
x = ...;
}
func printX() {
updateX();
print(x);
}
}
我们可以看到,在printX⽅法⾥⾯,updateX和print⽅法之间并没有值的传递,乍⼀看我们可能不知道x从哪⾥来的,导致程序的可读性降低了。⽽如果你使⽤局部变量⽽不是类成员来传递信息,那么这两个函数就不需要依赖于某⼀个类,⽽且更加容易理解,不易出错:
func updateX() -> String{
x = ...;
return x;
}
func printX() {
String x = updateX();
print(x);
}
注释
优秀的代码⼤部分是可以⾃描述的,我们完全可以⽤程代码本身来表达它到底在⼲什么,⽽不需要注释的辅助。但并不是说⼀定不能写注释,有以下三种情况⽐较适合写注释:
1.公共接⼝(注释要告诉阅读代码的⼈,当前类能实现什么功能)。
2.涉及到⽐较深层专业知识的代码(注释要体现出实现原理和思想)。
3.容易产⽣歧义的代码(但是严格来说,容易让⼈产⽣歧义的代码是不允许存在的)。
除了上述这三种情况,如果别⼈只能依靠注释才能读懂你的代码的时候,就要反思代码出现了什么问题。
最后,对于注释的内容,相对于“做了什么”,更应该说明“为什么这么做”。
4.注释格式。
文件注释:采用Xcode自动生成的注释格式。
//
// AppDelegate.h
// 项目名称
//
// Created by 开发者姓名 on 2018/6/8.
// Copyright © 2018年 公司名称. All rights reserved.
//
import注释:如果有一个以上的import语句,对这些语句进行分组,每个分组的注释是可选的。
// Framework
#import <UIKit/UIKit.h>
// Model
#import "WTUser.h"
// View
#import "WTView.h"
方法注释:Xcode8之后快捷键自动生成(option + command + /)。
/**
* <#Description#>
* @param application <#application description#>
* @param launchOptions <#launchOptions description#>
* @return <#return value description#>
*/
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
/// <#Description#>
/// @param application <#application description#>
/// @param launchOptions <#launchOptions description#>
/// @return <#return value description#>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
代码块注释:单行的用 “// + 空格 或者/// + 空格 ” 开头, 多行用“/* */”。
/// + 空格 和 /* */的注释方式,调用时会提示
代码结构与排版
- 声明文件:方法顺序和实现文件的顺序保持一致,根据需要用”#pragma mark -“将方法分组。
- 实现文件:必须用”#pragma mark -“将方法分组。分组前后优先级:Lifecycle方法 > Public方法 > UI方法 > Data方法 > Event方法 > Private方法(逻辑处理等) > Delegate方法 > 部分Override方法 > Setter方法 > Getter方法
#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 {}
#pragma mark - Public
- (void)refreshData {}
#pragma mark - UI
- (void)initSubViews {}
#pragma mark - Data
- (void)initData {}
- (void)constructData {}
#pragma mark - Event
- (void)clickButton:(UIButton *)button {}
#pragma mark - Private
- (CGFloat)calculateHeight {}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {}
#pragma mark - Override
- (BOOL)needNavigationBar {}
#pragma mark - Setter
- (void)setWindow:(UIWindow *)window {}
#pragma mark - Getter
- (UIWindow *)window {}
Code Review
换⾏、注释、⽅法⻓度、代码重复等这些是通过机器检查出来的问题,是⽆需通过⼈来做的。
⽽且除了审查需求的实现的程度,bug是否⽆处藏身以外,更应该关注代码的设计。⽐如类与类之间的耦合程度,设计的可扩展性,复⽤性,是否可以将某些⽅法抽出来作为接⼝等等。