iOS

Objective-C代码规范

为什么要有代码规范?

对于团队,如果代码风格不统一,阅读或修改同事的代码会非常困难,造成潜在的风险。

对于个人,代码规范是对自身编码习惯的一种监督,如果没有这种监督,有时候因为偷懒,会写出难看的代码,时间长了自己都看不懂。这样对于代码的维护性是不利的。

代码规范的内容?

代码规范包含的范围十分广泛。从一个变量的命名到一个类的设计,我觉得都属于代码规范的范畴。从实践的角度,可以把代码规范分成两个部分:

第一部分是规则,即一定要这么做。这里面没有对错,但需要统一。包含变量的命名、函数的命名、模块的组织、代码块的组织、宏、枚举、常量的声明、函数的粒度。

第二部分是风格,即一种模式化的代码设计结构。我们实现某个功能时,往往不止一种实现方式。每一种实现方式没有绝对的高低之分,不同角度的解读,就会有不同的偏好。所以这个层面上的代码规范,只能求同存异。但是不管怎样,每个人必须要有一致性的风格。就像不同的小区可以有不同的风格,但同一个小区只能有一种设计风格。风格包含代码设计中的抽象概念,比如接口、继承等等。

为什么要有代码规范?

对于团队,如果代码风格不统一,阅读或修改同事的代码会非常困难,造成潜在的风险。

对于个人,代码规范是对自身编码习惯的一种监督,如果没有这种监督,有时候因为偷懒,会写出难看的代码,时间长了自己都看不懂。这样对于代码的维护性是不利的。

代码规范的内容?

代码规范包含的范围十分广泛。从一个变量的命名到一个类的设计,我觉得都属于代码规范的范畴。从实践的角度,可以把代码规范分成两个部分:

第一部分是规则,即一定要这么做。这里面没有对错,但需要统一。包含变量的命名、函数的命名、模块的组织、代码块的组织、宏、枚举、常量的声明、函数的粒度。

第二部分是风格,即一种模式化的代码设计结构。我们实现某个功能时,往往不止一种实现方式。每一种实现方式没有绝对的高低之分,不同角度的解读,就会有不同的偏好。所以这个层面上的代码规范,只能求同存异。但是不管怎样,每个人必须要有一致性的风格。就像不同的小区可以有不同的风格,但同一个小区只能有一种设计风格。风格包含代码设计中的抽象概念,比如接口、继承等等。

代码的规则

规则如同法律,尽管从心底里很多人不认可,但每个人都必须遵守。看起来大家都受了束缚,但是对整个社会以及每一个人都是利大于弊。

变量命名

变量命名历史上产生过很多方法,比较著名的有匈牙利命名法、驼峰命名法、下划线命名法。

  • 匈牙利命名法:szUserName
  • 驼峰命名法:userName
  • 下划线命名法:user_name

用Objective-C开发,我们就参考苹果的官方命名方式即可,苹果采用的是驼峰命名法。

苹果的变量命名方式常常被其它语言的开发者所吐槽,因为它的名有时候是在太长了!!!下面是3个最长的Objective-C属性名:
[56] automaticallyEnablesStillImageStabilizationWhenAvailable
[54] availableMediaCharacteristicsWithMediaSelectionOptions
[49] outputObscuredDueToInsufficientExternalProtection
变量声明最重要的是清晰其次才考虑长短,如果为了简短而含糊不清,是不可取的。

Objective-C项目的命名有多长

拿现有代码举例

变量含义 Not Good Good
昵称输入框 tfNickName nickNameTextField
密码ImageView imvPass passwordImageView
我的搜索条 mysearchBar mySearchBar

类变量
前面加下划线,和系统框架保持一致

1
2
3
4
5
6
7
8
9
10
11
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer> {
@package
UIView *_view;
UITabBarItem *_tabBarItem;
UINavigationItem *_navigationItem;
NSArray *_toolbarItems;
NSString *_title;

NSString *_nibName;
NSBundle *_nibBundle;
......

现有的例子

1
2
3
4
5
6
7
8
9
10
@interface CouponListTableViewCell : UITableViewCell
{
UIImageView *imageIcon;
UIImageView *imageCollect;
UILabel *labelName;
UILabel *labelPriceSale;
UILabel *labelPriceOrigin;
UILabel *labelSaleNumber;
UIImageView *imageArrow;
}

类变量与属性
属性是Objective-C的一种高级语法,属性可以完全替换类变量,并且提供更强大的访问控制(strong、weak、nonatomic、readonly…),而且属性也只需要用一行就能搞定,所以为什么还要用类变量呢?
属性声明后不用写@synthesize,且会自动生成类变量。资料>

例外
一些局部变量允许用最简单的方式命名:i、j、temp
一些常用的缩写可以不用驼峰:RMB,OBD

函数命名

Objective-C中的函数不同于其他语言,函数名是按参数隔开的。初看起来是有点怪,但是习惯了就会发现这样和命名方式可读性极好,就像一句话一样。所以当你的函数名不能流利的读出来的话,那么它的命名肯定就有问题。
还是先举系统的例子

1
- (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration

这里面值得注意的几点:

  • 最左边的(+、-)和返回类型的左括号之间有一个空格,返回类型的右括号与第一个参数之间没有空格;
  • 函数名中每一段描述都以小写字母开头,描述的写法要考虑清晰明确,参考变量命名;
  • 如果函数名过长,应该换行,按冒号对齐;
  • 从描述中可以看出对应参数的意义。

再看看我们自己的命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

1、最后一个参数描述Selector首字母大写了
+(UIButton *)createButton:(MUButtonItemAttribute *)attr target:(id)target Selector:(SEL)sel;
2、有一些多余的空格
+ (AppDelegate *) appDelegate;
- (void)hideTabbar :(BOOL)status;
3、加减号和括号之间要有空格
-(void)pushEvent:(NSDictionary*)dictionary target:(id)target;
4、参数名和参数要对应,含义清晰
- (void)setNavBackButton:(NSString *)string;
- (id)init:(NSString*)nickName phoneNumber:(NSString *)phoneNumber;
- (void)setTextFieldBackGroud:(CGRect)rect;
- (id)init:(NSInteger)flag;
- (void)requestHealthRecord:(NSString *)typeStr;
5、函数名过长要换行
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier containingTableView:(UITableView *)containingTableView leftUtilityButtons:(NSArray *)leftUtilityButtons rightUtilityButtons:(NSArray *)rightUtilityButtons;
6、命名风格混乱
+ (NSInteger) GetServerBack:(NSString *)serverName
path_Param:(NSMutableDictionary*)path_Param
query_Param:(NSMutableDictionary*)query_Param
body_Param:(NSMutableDictionary*)body_Param
method:(NSInteger)method
returnValue:(NSMutableDictionary*)returnValue;
7、参数之间用and、with,多此一举
- (id)initWithFrame:(CGRect)frame andImageView:(UIImageView*)imageView withImageFrame:(CGRect)imageInitFrame withImageUrl:(NSURL*)imageUrl;

命名很重要!!!
良好的命名是写出高质量代码的前提,命名的时候除了符合以上的规范,还需要找到合适的单词来表达含义,不要为了一时之快用拼音,也千万不要出现数字。

代码模块组织

这个代码模块的组织是指文件级别的,即我们应该如何划分包。
现有的包结构是有些问题的,导致我们找某个功能的实现文件比较麻烦。
参考别人的经验,个人觉得这样的划分还是不错的。

文件的命名
我们自己创建的文件,应该有统一的前缀。比如HJMainViewController,这也符合系统的命名规范。Objective-C没有namespace,所以这样做的好处是当你引用了第三方的代码时,防止重名。而且当我们自己的代码越来越大时,内部也会出现冲突。

一个文件一个类
好处:

  • 减少单个文件的长度
  • 定位文件更方便
  • 便于重用

代码块的组织

作为一个程序员,大部分时间实在看代码而不是在写。所以代码的可读性严重影响到我们的工作效率。
当你看到超过1000行的实现文件时,你还有看下去的欲望吗?
所以千万不要写出超过1000行的代码,有的话也得重构掉

当代码控制到1000行以内后,只是具备了可读的基本条件。我们的屏幕一般只能一次显示50行代码,从头看到尾就是20屏,常常会看了后面忘了前面。所以我们要把一个文件里面的所有函数有秩序的组织起来,让人不用重头看到尾,就能很快定位到想看的地方。
这里提供一个小办法:用#pragma mark宏根据功能区分开:

1
2
3
4
5
6
7
8
9
#pragma mark - Life Circle
#pragma mark - Public Interface
#pragma mark - UI Actions
#pragma mark - Business Logic
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - UIScrollViewDelegate
#pragma mark - Notification Handle
#pragma mark - Private Method

宏、枚举、常量的声明

宏的命名方式参考变量的命名方式,大写,用下划线分开

1
TARGET_OS_IPHONE

枚举也有很多写法,这里推荐系统的方式

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSTextAlignment) {
NSTextAlignmentLeft = 0, // Visually left aligned
NSTextAlignmentRight = 1, // Visually right aligned
NSTextAlignmentCenter = 2, // Visually centered
NSTextAlignmentJustified = 3, // Fully-justified. The last line in a paragraph is natural-aligned.
NSTextAlignmentNatural = 4, // Indicates the default alignment for script
}

常量以k开头,系统命名风格

1
2
3
4
extern NSString *const kAPNetworkDidSetupNotification;     // 建立连接
extern NSString *const kAPNetworkDidCloseNotification; // 关闭连接
extern NSString *const kAPNetworkDidRegisterNotification; // 注册成功
extern NSString *const kAPNetworkDidLoginNotification; // 登录成功

如果是数值常量,用宏的话,也可以用k开头

1
#define kDistancePoint  10.0f   // 圆点、标志和正文的间距

函数的粒度

知道我们现在最长的函数有多少行吗?655行。

1
- (void)getShopDetail

一个函数最好不超过一屏(50行),千万不能超过两屏(100行)。
因为从设计的角度来说,一个函数只需要干一件事,所以50行一般是狗的,超长的函数往往干了很多事情,或者干了意见很大的事(拆成多件小事,交给子函数去干)。

其它

  • Log:不要使用NSLog,提交了会因想到别人,而且最后打包删起来麻烦。需要找一个Log工具,MyNSLog功能不够。
  • 代码中尽量不要出现数字:据上下文推测出来的,还是计算的出为好(现在界面里面很多地方确实要写死数字,适配iPhone6会很困难)

    1
    _contentBkg = [[UIView alloc] initWithFrame:CGRectMake(0, 1 / [UIScreen mainScreen].scale, 320, 68 - 1)];
  • 避免复杂的表达式

    1
    2
    3
    4
    5
    6
    7
    if (tfPassword.text!=nil &&
    ![tfPassword.text isEqualToString:@""] &&
    tfRequestNumber.text!=nil &&
    ![tfRequestNumber.text isEqualToString:@""]
    )
    {
    }
  • 一个变量不在多个函数中出现,不要作为类变量:

    1
    2
    MBProgressHUD* hudProgress;
    int result;
  • 每个类的.h开头的地方要有注释,说明这个类是干嘛的

    1
    2
    3
    4
    5
    /**
    * SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed
    * asynchronous so it doesn’t add unnecessary latency to the UI.
    */
    @interface SDImageCache : NSObject
  • if后面的语句哪怕只有一行,都加括号

    1
    2
    3
    4
    5
    if ([key rangeOfString:@"热"].location != NSNotFound) {
    titleLabel.text = @"热门城市";
    }
    else
    titleLabel.text = key;

代码的风格

风格如同习俗,不同地区有不同的习俗。你觉得入乡随俗好,就入乡随俗,这样避免了一些无谓的麻烦;但是你若坚持原有的习俗,社会也是能接受的,毕竟这些东西都是发展变化的,没有固定的模式,只有更好的模式。

接口

这里的接口指的是一个类公开的一组方法,良好设计的类是高内聚低耦合的,所以接口必须是简洁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface PaySucceedViewController : LCBaseViewController

@property (nonatomic,retain) NSString *isSuccess;

@property (nonatomic,retain) NSString *shopName;

@property (nonatomic,retain) NSString *detailinfo;

@property (nonatomic,retain) NSString *amount;

@property (nonatomic,retain) NSString *omsOrderId;
/*!
* 支付订单类型
*
* @since
*/
@property (nonatomic) ORDER_CATEGORY mCategory;

@end

上面的例子是一个ViewController,但是它暴露了太多属性,属性很零碎,传值的时候要一个一个传。其实这里属性应该属于一个叫订单Model的对象,把这些属性封装在OrderModel中,复用性和可维护性都会好很多。(但是现在代码整体结构没有Model层,这里需要很大的重构)

继承

继承不是为了少写代码,它的目的是抽象,顺便少写了代码。
举NavigationBar的例子,用宏可以实现,用基类也可以实现,但基类抽象出了一个共有的接口,以后发生共性的变化够可以通过修改基类来实现,你可以在基类里加一组方法,而宏做不到。

其它

代码规范的路还长着,先走出第一步吧!

参考资料

Apple代码规范