这个小系列是从 "Zen and the Art of the Objective-C Craftsmanship"中 进行的摘抄,共分成6篇,大部分是讲代码风格及美化,偶尔看看也不错。
- 原文GitHub地址:
https://github.com/objc-zen/objc-zen-book - 中文版GitHub地址:
https://github.com/oa414/objc-zen-book-cn
一. 类名
类名应该以三个大写字母作为前缀(双字母前缀为Apple的类预留)
不仅仅是类,公开的常量、Protocol等的前缀都为相同的三个大写字母。
-
当你创建一个子类的时候,你应该把说明性的部分放在前缀和父类名的中间。
例如:
如果你有一个 ZOCNetworkClient 类,子类的名字会是ZOCTwitterNetworkClient (注意 "Twitter" 在 "ZOC" 和 "NetworkClient" 之间); 按照这个约定, 一个UIViewController 的子类会是 ZOCTimelineViewController.
二. Initializer和dealloc
推荐的代码组织方式是将dealloc方法放在实现文件的最前面(直接在@synthesize以及@dynamic之后),init应该跟在dealloc方法后面。
如果有多个初始化方法,那么指定初始化方法应该放在最前面,间接初始化方法跟在后面。
如今有了ARC,dealloc方法几乎不需要实现,不过把init和dealloc放在一起,强调它们是一对的。通常在init方法中做的事情需要在dealloc方法中撤销。
-
关于指定初始化方法(designated initializer)和间接初始化方法(secondary initializer)
Objective-C 有指定初始化方法(designated initializer)和间接(secondary initializer)初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化的初始化方法。
@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 初始化方法。
一个类应该有且只有一个 designated 初始化方法,其他的初始化方法应该调用这个 designated 的初始化方法(有例外)。
三. 当定义一个新类的时候有三个不同的方式:
- 不需要重载任何初始化函数
- 重载 designated initializer
- 定义一个新的 designated initializer
第一种方式不需要增加类的任何初始化逻辑,也就是说在类中不必重写父类的初始化方法也不需要其他操作。
第二种方式要重载父类的指定初始化方法。例子:
@implementation ZOCViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call to the superclass designated initializer
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization (自定义的初始化过程)
}
return self;
}
@end
这个例子中,ZOCViewController继承自UIViewController,这里我们有一些其他的需求(比如希望在初始化的时候给一些成员变量赋值),所以需要重写父类的指定初始化方法initWithNibName:bundle:方法。
注意,如果在这里并没有重载这个方法,而是重载了父类的init方法,那么会是一个错误。
因为在创建这个类(ZOCViewController)的时候,会调用initWithNib:bundle:这个方法,所以我们重载这个方法,首先保证父类初始化成功,然后在这个方法中进行额外的初始化操作。但是如果重载init方法,在创建这个类的时候,并不会调用init方法(调用的是initWithNib:bundle:这个指定初始化方法)。
第三种方式是希望提供自己的类初始化方法,应该遵守下面三个步骤来保证正确性:
- 定义你的 designated initializer,确保调用了直接超类的 designated initializer。
- 重载直接超类的 designated initializer。调用你的新的 designated initializer。
- 为新的 designated initializer 写文档。
很多开发者会忽略后两步,这不仅仅是一个粗心的问题,而且这样违反了框架的规则,而且可能导致不确定的行为和bug。
正确的例子:
@implementation ZOCNewsViewController
- (id)initWithNews:(ZOCNews *)news
{
// call to the immediate superclass's designated initializer (调用直接超类的 designated initializer)
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// Override the immediate superclass's designated initializer (重载直接父类的 designated initializer)
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
// call the new designated initializer
return [self initWithNews:nil];
}
@end
很多开发者只会写第一个自定义的初始化方法,而不重载父类的指定初始化方法。
在第一个自定义的初始化方法中,因为我们要定义自己的指定初始化方法,所以在最开始的时候首先要调用父类的指定初始化方法以保证父类都初始化成功,这样ZOCNewsViewController才是可用状态。(因为父类是通过initWithNibName:bundle:这个指定初始化方法创建的,所以我们要调用父类的这个方法来保证父类初始化成功)。然后在后面给_news赋值。
如果仅仅是这样做是存在问题的。调用者如果调用initWithNibName:bundle:来初始化这个类也是完全合法的,如果是这种情况,那么initWithNews:这个方法永远不会被调用,所以_news = news也不会被执行,这样导致了不正确的初始化流程。
解决方法就是需要重载父类的指定初始化方法,在这个方法中返回新的指定初始化方法(如例子中做的那样),这样无论是调用哪个方法都可以成功初始化。
-
间接初始化方法是一种提供默认值、行为到初始化方法的方法。
你不应该在间接初始化方法中有初始化实例变量的操作,并且你应该一直假设这个方法不会得到调用。我们保证的是唯一被调用的方法是 designated initializer。
这意味着你的 secondary initializer 总是应该调用 Designated initializer 或者你自定义(上面的第三种情况:自定义Designated initializer)的 self的 designated initializer。有时候,因为错误,可能打成了 super,这样会导致不符合上面提及的初始化顺序。
也就是说,你可能看到一个类有多个初始化方法,实际上是一个指定初始化方法(或多个,比如UITableViewController就有好几个)+多个间接初始化方法。这些简洁初始化方法可能会根据不同的参数做不同的操作,但是本质上都是调用指定初始化方法。所以说,间接初始化方法是有可能没有调用到的,但是指定初始化方法是会调用到的(并不是每一个都会调用到,但是最后调用的一定是一个指定初始化方法)。(这里又可以引申到上面提到的问题,我们可以直接重写父类的指定初始化方法,也可以自定义初始化方法(在这个方法中需要用到self = [super 父类初始化方法]这种形式的代码),并且如果是自定义初始化方法,还应该重写从父类继承的初始化方法来返回我们的自定义初始化方法…)。
总之就是,如果重写父类的指定初始化方法首先需要调用父类的相应初始化方法;如果增加自定义指定初始化方法,首先在新增的自定义指定初始化方法中调用父类的相应初始化方法,然后需要重写父类的指定初始化方法,在重写的方法中调用刚刚添加的自定义指定初始化方法。
-
补充
一个类可能有多个指定初始化方法,也有可能只有一个指定初始化方法。
以UITableViewController为例,我们可以看到:
- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER; - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER; - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
它有三个指定初始化方法,我们刚才说,当子类从父类继承并重写初始化方法,首先需要调用父类的初始化方法,但是如果一个类的初始化方法有多个,那么需要调用哪个呢?
事实上不同的创建方式要调用不同的指定初始化方法。
比如,我们以Nib的形式创建UITableViewController,那么最后调用的就是- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil这个指定初始化方法�;如果我们以Storyboard的形式创建,那么最后调用的就是- (instancetype)initWithCoder:(NSCoder *)aDecoder这个指定初始化方法。如果以代码的形式创建,那么最后调用的就是- (instancetype)initWithStyle:(UITableViewStyle)style这个指定初始化方法。所以不同的情况需要重写不同的指定初始化方法,并且重写的时候首先要调用父类相应的指定初始化方法(比如重写initWithCoder:方法,那么首先self = [super initWithCoder:…],都是一一对应的)。
再以UIViewController为例,我们以Nib的形式创建UIViewController,那么最后调用的是- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil,这与UITableViewController是一样的;如果我们以Storyboard的形式创建,那么最后调用的是- (instancetype)initWithCoder:(NSCoder *)aDecoder,这与UITableViewController也是一样的;但是如果我们以代码的形式创建UIViewController(eg: CYLViewController *vc = [[CYLViewController alloc] init]; CYLViewController继承自UIViewController),那么它最后调用的实际是- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil,这与UITableViewController是不一样的,因为UIViewController并没有- (instancetype)initWithStyle:(UITableViewStyle)style这个方法,所以当用代码创建的时候,最后调用的也是initWithNibName:bundle这个指定初始化方法,并且参数自动设置为nil。
所以现在反过头来再看UITableViewController,当使用代码的方式创建的时候(eg: CYLTableViewController *tvc = [[CYLTableViewController alloc] init]; 或者 CYLTableViewController *tvc = [[CYLTableViewController alloc] initWithStyle: UITableViewStylePlain]; ),它会调用initWithStyle:这个方法,但是如果你也实现了initWithNibName:bundle:这个方法,你会发现这个方法也被调用了。因为UITableViewController继承自UIViewController,所以当用代码创建的时候,最后也会掉用到initWithNIbName:bundle:(因为UIViewController就是这么干的)。
所以用代码创建UITableViewController的时候,它会调用initWithNibName:bundle:和initWithStyle:这两个方法。
四. 属性
属性要尽可能描述性地命名,并且使用驼峰命名。
-
关于”*”的位置:
// 推荐 NSString *text; // 不推荐 NSString* text; NSString * text;
注意,这个习惯和常量并不同。
static NSString * const ...
你永远不能在 init (以及其他初始化函数)里面用 getter 和 setter 方法,你应该直接访问实例变量。记住一个对象是仅仅在 init 返回的时候,才会被认为是初始化完成到一个状态了。
-
当使用 setter/getter 方法的时候尽量使用点符号。
// 推荐 view.backgroundColor = [UIColor orangeColor]; [UIApplication sharedApplication].delegate; // 不推荐 [view setBackgroundColor:[UIColor orangeColor]]; UIApplication.sharedApplication.delegate;
使用点符号会让表达更加清晰并且帮助区分属性访问和方法调用。
-
属性定义
@property (nonatomic, readwrite, copy) NSString *name;
属性的参数应该按照这个顺序排列: 原子性,读写和内存管理。
习惯上修改某个属性的修饰符时,一般从属性名从右向左搜索需要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操作
你必须使用 nonatomic,除非特别需要的情况。在iOS中,atomic带来的锁特别影响性能。
-
如果想要一个公开的getter和私有的setter,你应该声明公开的属性为 readonly 并且在类扩展总重新定义通用的属性为 readwrite 的。
//.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;
任何可以用来用一个可变的对象设置的((比如 NSString,NSArray,NSURLRequest))属性的的内存管理类型必须是 copy 的。(原文中是这样说的,但是我理解的话并不是绝对的。如果不想让原来的可变对象影响到类的这个相应属性,那么就需要用copy,这样在赋值的时候可变对象会首先进行copy完成深拷贝,再把拷贝出的值赋给类的属性,这样就能保证类属性和原来的可变对象影响并不影响。但是如果想让类属性对原来的可变对象是一个强引用,指向这个可变对象,那么会用strong。)
-
你应该同时避免暴露在公开的接口中可变的对象,因为这允许你的类的使用者改变类自己的内部表示并且破坏类的封装。你可以提供可以只读的属性来返回你对象的不可变的副本。
/* .h */ @property (nonatomic, readonly) NSArray *elements /* .m */ - (NSArray *)elements { return [self.mutableElements copy]; }
-
虽然使用懒加载在某些情况下很不错,但是使用前应当深思熟虑,因为懒加载通常会产生一些副作用。(但是懒加载还是比较常用的,比如下面的例子)
副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。
- (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; }
五. 方法
-
参数断言
你的方法可能要求一些参数来满足特定的条件(比如不能为nil),在这种情况下啊最好使用 NSParameterAssert() 来断言条件是否成立或是抛出一个异常。
- (void)viewDidLoad { [super viewDidLoad]; [self testMethodWithAParameter:0]; } - (void)testMethodWithAParameter: (int)value { NSParameterAssert(value != 0); NSLog(@"正确执行"); }
在此例中, 如果传的参数为0,那么程序会抛出异常。
-
私有方法
永远不要在你的私有方法前加上 _ 前缀。这个前缀是 Apple 保留的。不要冒重载苹果的私有方法的险。
-
当你要实现相等性的时候记住这个约定:你需要同时实现isEqual 和 hash方法。如果两个对象是被isEqual认为相等的,它们的 hash 方法需要返回一样的值。但是如果 hash 返回一样的值,并不能确保他们相等。
@implementation ZOCPerson - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[ZOCPerson class]]) { return NO; } // check objects properties (name and birthday) for equality (检查对象属性(名字和生日)的相等性 ... return propertiesMatch; } - (NSUInteger)hash { return [self.name hash] ^ [self.birthday hash]; } @end
你总是应该用 isEqualTo<#class-name-without-prefix#>: 这样的格式实现一个相等性检查方法。如果你这样做,会优先调用这个方法来避免上面的类型检查。
所以一个完整的 isEqual 方法应该是这样的:
- (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[ZOCPerson class]]) { return NO; } return [self isEqualToPerson:(ZOCPerson *)object]; } - (BOOL)isEqualToPerson:(Person *)person { if (!person) { return NO; } BOOL namesMatch = (!self.name && !person.name) || [self.name isEqualToString:person.name]; BOOL birthdaysMatch = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday]; return haveEqualNames && haveEqualBirthdays; }
所有文章
【objc-zen-book】1.条件语句&Case语句的注意
【objc-zen-book】2.命名
【objc-zen-book】3.类
【objc-zen-book】4.Category & NSNotification
【objc-zen-book】5.美化代码 & 代码组织
【objc-zen-book】6.Block & self的循环引用