這個小系列是從 "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的循環引用