阅读 《禅与 Objective-C 编程艺术》之后
小记一些比较重要的代码规范
黄金大道
当编写条件语句的时候,左边的代码间距应该是一个“黄金”或者“快乐”的大道。 这是说,不要嵌套if语句。多个 return 语句是OK的。这样可以避免 Cyclomatic 复杂性,并且让代码更加容易阅读。因为你的方法的重要的部分没有嵌套在分支上,你可以很清楚地找到相关的代码。
推荐:
- (void)someMethod {
if (![someOther 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
}
Enumerated Types 枚举类型
当使用 enum 的时候,建议使用新的固定的基础类型定义,因它有更强大的的类型检查和代码补全。 SDK 现在有一个 宏来鼓励和促进使用固定类型定义 - NS_ENUM()
*例子: **
typedef NS_ENUM(NSUInteger, ZOCMachineState) {
ZOCMachineStateNone,
ZOCMachineStateIdle,
ZOCMachineStateRunning,
ZOCMachineStatePaused
};
Constants 常量
常量应该使用驼峰命名法,并且为了清楚,应该用相关的类名作为前缀。
推荐:
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
不推荐:
static const NSTimeInterval fadeOutTime = 0.4;
常量应该尽量使用 in-line 的字符串字面值或者数字,这样便于经常用到的时候复用,并且可以快速修改而不用查找和替换。 常量应该用 static 声明,并且不要使用 #define,除非它就是明确作为一个宏来用的。
推荐:
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
不推荐:
#define CompanyName @"Apple Inc."
#define magicNumber 42
常量应该在 interface 文件中这样被声明:
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
并且应该在实现文件中实现它的定义。
你只需要为公开的常量添加命名空间前缀。即使私有常量在实现文件中可能以不同的模式使用,你也不需要坚持这个规则了。
方法
对于方法签名,在方法类型 (-/+ 符号)后应该要有一个空格。方法段之间也应该有一个空格(来符合 Apple 的规范)。在参数名称之前总是应该有一个描述性的关键词。
使用“and”命名的时候应当更加谨慎。它不应该用作阐明有多个参数,比如下面的initWithWidth:height: 例子:
推荐:
- (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;
不推荐:
- (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.
字面值
NSString, NSDictionary, NSArray, 和 NSNumber 字面值应该用在任何创建不可变的实例对象。特别小心 nil 不能放进 NSArray 和 NSDictionary 里,这会导致 Crash。
Initializer 和 dealloc 初始化
推荐的代码组织方式:将 dealloc
方法放在实现文件的最前面(直接在 @synthesize
以及 @dynamic
之后),init
应该放在 dealloc
之后。如果有多个初始化方法, designated initializer 应该放在第一个,secondary initializer 在之后紧随,这样逻辑性更好。 如今有了 ARC,dealloc 方法几乎不需要实现,不过把 init 和 dealloc 放在一起可以从视觉上强调它们是一对的。通常,在 init 方法中做的事情需要在 dealloc 方法中撤销。
init
方法应该是这样的结构:
- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
为什么设置 self
为 [super init]
的返回值,以及中间发生了什么呢?这是一个十分有趣的话题。
让我们后退一步:我们曾经写了类似 [[NSObject alloc] init]
的表达式, alloc
和 init
区别慢慢褪去 。一个 Objective-C 的特性叫 两步创建 。 这意味着申请分配内存和初始化是两个分离的操作。
-
alloc
表示对象分配内存,这个过程涉及分配足够的可用内存来保存对象,写入isa
指针,初始化 retain 的计数,并且初始化所有实例变量。 -
init
是表示初始化对象,这意味着把对象放到了一个可用的状态。这通常是指把对象的实例变量赋给了可用的值。
alloc
方法会返回一个合法的没有初始化的实例对象。每一个发送到实例的信息会被翻译为名字是 self
的 alloc
返回的指针的参数返回的 objc_msgSend()
的调用。这样之后 self
已经可以执行所有方法了。 为了完成两步创建,第一个发送给新创建的实例的方法应该是约定俗成的 init
方法。注意 NSObject
的 init
实现中,仅仅是返回了 self
。
关于 init
有一个另外的重要的约定:这个方法可以(并且应该)在不能成功完成初始化的时候返回 nil
;初始化可能因为各种原因失败,比如一个输入的格式错误,或者未能成功初始化一个需要的对象。 这样我们就理解了为什么需要总是调用 self = [super init]
。如果你的超类没有成功初始化它自己,你必须假设你在一个矛盾的状态,并且在你的实现中不要处理你自己的初始化逻辑,同时返回 nil
。如果你不是这样做,你看你会得到一个不能用的对象,并且它的行为是不可预测的,最终可能会导致你的 app 发生 crash。
重新给 self
赋值同样可以被 init
利用为在被调用的时候返回不同的实例。一个例子是 类簇 或者其他的返回相同的(不可变的)实例对象的 Cocoa 类。
Categories
虽然我们知道这样写很丑, 但是我们应该要在我们的 category 方法前加上自己的小写前缀以及下划线,比如- (id)zoc_myCategoryMethod
。 这种实践同样被苹果推荐。
这是非常必要的。因为如果在扩展的 category 或者其他 category 里面已经使用了同样的方法名,会导致不可预计的后果。实际上,实际被调用的是最后被实现的那个方法。
如果想要确认你的分类方法没有覆盖其他实现的话,可以把环境变量 OBJC_PRINT_REPLACED_METHODS 设置为 YES,这样那些被取代的方法名字会打印到 Console 中。现在 LLVM 5.1 不会为此发出任何警告和错误提示,所以自己小心不要在分类中重载方法。
一个好的实践是在 category 名中使用前缀。
例子
@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end
不要这样做
@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end
分类可以用来在头文件中定义一组功能相似的方法。这是在 Apple的 Framework 也很常见的一个实践(下面例子的取自NSDate
头)。我们也强烈建议在自己的代码中这样使用。
我们的经验是,创建一组分类对以后的重构十分有帮助。一个类的接口增加的时候,可能意味着你的类做了太多事情,违背了类的单一功能原则。
之前创造的方法分组可以用来更好地进行不同功能的表示,并且把类打破在更多自我包含的组成部分里。
@interface NSDate : NSObject <NSCopying, NSSecureCoding>
@property (readonly) NSTimeInterval timeIntervalSinceReferenceDate;
@end
@interface NSDate (NSDateCreation)
+ (instancetype)date;
+ (instancetype)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
+ (instancetype)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
// ...
@end
网友评论