对于编程开发来说,编码规范是不可或缺的一个环节。在iOS开发领域,苹果也有官方的编码规范文档:《Coding Guidelines for Cocoa》。尽管对官方的这些权威指南,每一个iOS开发人员都应当去遵守,但在不少视频教程、文章、示例代码中,依然经常可以看到违反编码规范基本原则的情况。本文将列出3个经常被忽略的规范原则,希望大家在日常的开发中能留意纠正。
一、使用get开头的方法来返回数据
对于从其他语言转向iOS(尤其是从C++/Java转过来的)的开发人员,很容易犯这个错。在其他语言中,很多都习惯用getXXX
作为getter方法,许多人也把这个习惯自然而然的带到了iOS,因此他们经常会在iOS中写下类似的代码:
- (XxxModel *)getXxxModel;
百度导航SDK:
+ (BNCoreServices*)GetInstance;
但iOS是个“异类”,明确规定不提倡这么做:
Use “get” only for methods that return objects and values indirectly. You should use this form for methods only when multiple items need to be returned.
只有方法需要间接返回多个值的情况下才使用 get。
那么所谓的“间接返回”指的是什么?比如下面的方法:
// NSString
- (void)getLineStart:(nullable NSUInteger *)startPtr end:(nullable NSUInteger *)lineEndPtr contentsEnd:(nullable NSUInteger *)contentsEndPtr forRange:(NSRange)range;
NSUInteger startPtr;
NSUInteger lineEndPtr;
NSUInteger contentsEndPtr;
NSString *testText = @"这是一段很长的测试文本\n这是第二段测试文本\n";
[testText getLineStart:&startPtr end:&lineEndPtr contentsEnd:&contentsEndPtr forRange:NSMakeRange(2, 5)];
NSLog(@"startPtr:%d lineEndPtr:%d contentsEndPtr:%d", startPtr, lineEndPtr, contentsEndPtr);
// 输出:
// startPtr:0 lineEndPtr:12 contentsEndPtr:11
可以看到,这个方法通过引用的方式,将行开头位置、行结束位置、行内容结束位置,这3个信息返回回来。这种做法可以说师承C语言,因为方法只能返回单个值,要返回多个值,就会用这样的变通方式来实现。
解决方法:
- 去掉get,这种方法最简单,也是苹果规范中提倡的方法
- (XxxModel *)xxxModel;
+ (instancetype)instance;
- 使用其他单词来代替get,如:retrieve,但这种方法可能让人更不习惯
- (XxxModel *)retrieveXxxModel;
+ (instancetype)retrieveInstance;
二、使用init开头的方法却不返回实例对象
有些开发人员习惯在ViewController中写类似下面的方法:
- (void)initData;
- (void)initViews;
另外,在第三方SDK中也经常会看到init开头的方法,比如百度导航SDK:
- (void)initServices:(NSString *)ak;
在Objective-C Automatic Reference Counting (ARC)文档中提到了method family的概念,init开头的方法属于初始化系列的方法,规范上来说,应该要返回相应的Objective-C对象。
但是规范也意识到在现实的开发中,业已存在许多不符合规范的做法,出于务实的考虑,规范也对这一情况做出了额外的规定:
There are a fair number of existing methods with init-like selectors which nonetheless don’t follow the init conventions. Typically these are either accidental naming collisions or helper methods called during initialization.
……
Note that a method with an init-family selector which returns a non-Objective-C type (e.g. void) is perfectly well-formed; it simply isn’t in the init family.
也就是说,使用init开头的方法,但没有返回对象(返回值声明为void)将被视为普通方法,不会产生什么影响。
解决方法:
- 保留原样。这种命名方式规范上来说,会被视为普通方法,不会产生什么危害
- 用其他单词替换init,如setUp:
setUpData
、setUpViews
。虽然以init开头也没有问题,但不使用init来命名可以使自己的代码更加“iOS化”
三、使用缩写
In general, you shouldn’t abbreviate names when you design your programmatic interface (see General Principles). However, the abbreviations listed below are either well established or have been used in the past, and so you may continue to use them.
……
You may use abbreviations more freely in argument names (for example, “imageRep”, “col” (for “column”), “obj”, and “otherWin”).
在设计编程接口时通常不应使用缩写,一些被广泛使用的缩写名称(比如alloc、init)除外。而在方法参数中,则可以放宽限制,使用缩写。
然而在实际的开发中,经常可以看到缩写泛滥:
@property (nonatomic, strong) UIViewController *xxxCtrl;
@property (nonatomic, strong) NSString *oldPwd;
缩写的问题在于较难规范,如:password,有人用pw,有人用pwd,还有人用psd,这会导致有时看到缩写而不知其义的情况,只能结合上下文来判断。
解决方法
iOS中不提倡用缩写,SDK自带各种类的属性/方法多是完整拼出单词。我们在编码中也应遵守规范,尽量使用完整单词,而不是缩写。
不遵守规范的后果
诚然,不遵守规范通常不会导致什么灾难性的后果,规范更多是自觉自律、协作开发的要求。不过,对于iOS这个“异类”来说,有时不遵守规范却真的会导致灾难性的后果,只是这种情况现在并不常见。下面我将用一个例子来演示这种情况,如果你有兴趣,可以继续看下去。
首先,我们创建一个类TestCopyNameMethod,这个类有一个copyRightString方法:
// TestCopyNameMethod.h
@interface TestCopyNameMethod : NSObject
- (NSString *)copyRightString;
@end
// TestCopyNameMethod.m
#import "TestCopyNameMethod.h"
@implementation TestCopyNameMethod
- (NSString *)copyRightString {
return [NSMutableString stringWithString:@"this is a copyright"];
}
@end
下面我们调用这个方法,运行一下:
TestCopyNameMethod *testCopyNameMethod = [[TestCopyNameMethod alloc] init];
NSString *text = [testCopyNameMethod copyRightString];
NSLog(@"%@", text);
此时,程序会正确输出相应的值,运行正常,没有崩溃,也没有内存泄露。
接下来,我们将TestCopyNameMethod.m编译设置为非ARC(MRC)方式,在编译设置中加上-fno-objc-arc参数。
注意:设置完成最好先 Clean 一下项目,否则可能有些文件编译有缓存,导致运行看不到效果
再次运行,可以看到程序崩溃了:
只是将TestCopyNameMethod.m设置为MRC方式进行编译,为什么就会导致整个程序崩溃?
我们知道,即使是在MRC的时代,只要不是alloc、new、copy产生的对象,我们是不需要管理的。上面的代码中,copyRightString返回的是一个NSMutableString对象,而NSMutableString对象是调用stringWithString方法生成的,这个方法生成的对象是属于autorelease的,我们并不需要管理内存,按理说,程序不应该因此崩溃。那是什么原因导致的崩溃呢?
这跟我们混合使用ARC和MRC有关。
在ARC的机制中,对于调用copy开头的方法,ARC会认为这是要转移“所有权”的一个方法,因此它会自动在调用方法中插入相应的内存管理代码,这使得我们的调用代码就像:
TestCopyNameMethod *testCopyNameMethod = [[TestCopyNameMethod alloc] init];
NSString *text = [testCopyNameMethod copyRightString];
NSLog(@"%@", text);
[text release];
在最后多了一个release。
而TestCopyNameMethod.m已经被设置为MRC,copyRightString方法返回的对象则是一个autorelease对象,如果再做一次release就是过度释放,就导致程序崩溃。
注意:copyRightString中使用的是NSMutableString,不能使用NSString(即不能写成return @"this is a copyright";
)。因为NSString本身是有“缓存”的,retain和release都是无效的(如果你用%lu的格式打出NSString的retainCount值,会发现这个值非常大)。
其实,在运行之前,如果先对项目进行Analyze,会发现Analyze已经给出相应的警告:
而如果我们反过来,TestCopyNameMethod.m使用默认的ARC,而把调用copyRightString的.m文件(如:ViewController.m)设置为MRC,则会导致内存泄露。
再次强调,改完编译参数先 Clean 一下项目,否则可能因为“缓存”看不到效果
运行Analyze会给出内存泄露的警告:
用Profile运行也会检测到内存泄露:
这是因为TestCopyNameMethod.m设置为ARC时,ARC看到copyRightString这样以copy开头的方法,就会在返回的NSMutableString对象上加上retain(因为返回的NSMutableString对象是属于autorelease的,而copy方法意味着要转移“所有权”,所以会加上retain)。所以,在ARC的作用下,copyRightString就变成类似:
return [[NSMutableString stringWithString:@"this is a copyright"] retain];
而对于我们的调用代码,因为设置成了MRC方式,编译器并不会自动帮我们插入内存管理的代码(即[text release];
代码)。
NSString *text = [testCopyNameMethod copyRightString];
text本身由于ARC为copyRightString方法加上retain的缘故,已经拥有了返回对象的“所有权”,就必须负责相应的内存释放。而在MRC下,代码并没有做release,自然就导致内存泄露。
如果TestCopyNameMethod.m和调用代码都是ARC,则ARC在copyRightString方法加上了retain,又在调用代码中加上了release,正好在内存管理上达到了平衡,因此程序运行不会有问题。显然,现在的项目基本都是ARC的,所以,很少会遇到这方面的问题,但如果程序中使用的某些第三方库还是MRC的,就有可能导致问题。从中我们也看到了不遵守规范,有时确实会带来灾难性的后果。
对于这类问题的解决方法有两个:
- 修改方法名称,不要以copy开头
- 在.h文件的方法声明中,添加NS_RETURNS_NOT_RETAINED宏:
- (NSString *)copyRightString NS_RETURNS_NOT_RETAINED;
总结
还是按规范行事吧