初始化方法內(nèi)使用self有什么壞處?

場(chǎng)景描述
iOS初始化方法包括系統(tǒng)默認(rèn)的和自定義的,常見系統(tǒng)初始化方法有init, initWithFrame:, initWithNibName:bundle:等,自定義則是各式各樣。日常iOS項(xiàng)目開發(fā)過程中,我們經(jīng)常在類的初始化方法中初始化接下來類需要用到的一些必要的數(shù)據(jù)或界面。初始化方法內(nèi)使用self的場(chǎng)景大致有兩種,一是self調(diào)用方法,諸如:[self doSomething],二是屬性初始化,諸如:self.property = xxx。樣式大體如下:
@interface HHAnimal : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation HHAnimal
- (instancetype)init {
self = [super init];
if (self) {
self.name = @"HH";
[self doSomething];
}
return self;
}
- (void)doSomething {
}
@end
那么,為什么不建議在初始化方法內(nèi)self.property = xxx or [self doSomething]類似代碼呢?
問題分析
當(dāng)使用self.property = xxx時(shí),系統(tǒng)會(huì)幫我們做這兩件事情:
- 方法調(diào)用。
[self setProperty:xxx] - KVO。發(fā)送該屬性的變化給監(jiān)聽者
那么,合并一下[self doSomething],初始化調(diào)用self大體就分為方法調(diào)用和KVO。這兩件事情在一般情況下不會(huì)有問題,但在類在初始化的過程中,類處于一種部分初始化的狀態(tài),此時(shí)很有可能出現(xiàn)錯(cuò)誤。因?yàn)閳?zhí)行的方法體或者監(jiān)聽屬性值變化的對(duì)象會(huì)認(rèn)為當(dāng)前執(zhí)行過程是完全初始化的穩(wěn)定狀態(tài),當(dāng)類執(zhí)行體使用了還未初始化的數(shù)據(jù)時(shí),就可能發(fā)生數(shù)據(jù)錯(cuò)亂,程序異?;蛘遚rash。
下面我們舉例子說明~
舉例佐證
為了更好地說明,如下代碼中,假設(shè)我們有一個(gè)HHAnimal類,有三個(gè)屬性,age年齡,name動(dòng)物名字(目前是可讀), attrDescription表示用于展示的帶顏色的名字(只讀屬性),它是一個(gè)計(jì)算變量---根據(jù)年齡變化,名字的顏色不一樣。
@interface HHAnimal : NSObject
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) NSAttributedString *attrDescription;
@end
@implementation HHAnimal
- (instancetype)init {
self = [super init];
if (self) {
self.age = 12;
_name = @"HH";
}
return self;
}
- (void)updateAttrDescription {
NSDictionary *attrs = nil;
if (self.age < 18) {
attrs = @{NSForegroundColorAttributeName: [UIColor greenColor]};
} else {
attrs = @{NSForegroundColorAttributeName: [UIColor yellowColor]};
}
_attrDescription = [[NSAttributedString alloc] initWithString:self.name attributes:attrs];
}
@end
我們從出現(xiàn)錯(cuò)誤的概率,一級(jí)級(jí)的往上遞增?,F(xiàn)在我們希望在初始化后,attrDescription變量也被初始化。第一種添加方法 (直接在設(shè)置完默認(rèn)年齡后調(diào)用[self updateAttrDescription]):
- (instancetype)init {
self = [super init];
if (self) {
self.age = 12;
[self updateAttrDescription];
_name = @"HH";
}
return self;
}
有人會(huì)說,這種方法弱爆了,看我的:
- (void)setAge:(NSUInteger)age {
_age = age;
[self updateAttrDescription];
}

這種方式高級(jí)一些,將attrDescription跟年齡關(guān)聯(lián)起來。嗯,不錯(cuò)。但還是crash了,因?yàn)樵趫?zhí)行[self updateAttrDescription]時(shí),name為nil,而[NSAttributedString initWithString:attributes:]方法調(diào)用時(shí)若string為nil,蘋果爸爸直接給崩了。
那為什么你會(huì)這么寫呢?其中原因之一可能是因?yàn)槟悴恢涝撓到y(tǒng)方法不能將nil作為參數(shù),另外一個(gè)重要的點(diǎn)是你很明顯看到name在初始化方法中已經(jīng)被賦值了,這樣就不存在nil的問題。
這種顯而易見的場(chǎng)景在日常開發(fā)過程中,我們會(huì)很快發(fā)現(xiàn)。但,假設(shè)有一個(gè)子類HHHuman繼承了HHAnimal,它只能看到父類的.h文件(且聲明name是具有初始值的)。若HHHuman的實(shí)現(xiàn)內(nèi)重寫了age的setter方法,并將name當(dāng)做已初始化的一個(gè)變量使用的話,就可能引入崩潰等問題。
[UIViewController view]
除了上面介紹的一些例子,日常開發(fā)中一個(gè)更復(fù)雜也常見的例子要屬UIViewController的property--view。假設(shè)在初始化的過程中寫了self.view,如下所示:
@implementation ViewController
- (instancetype)initWithOrderId:(NSString *)orderId {
self = [super init];
if (self) {
NSLog(@"%@", self.view);
_orderId = orderId;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[HHNetUtil requestWithOrderId:_orderId completionBlock:...];
}
這樣寫,會(huì)有什么奇怪的事情發(fā)生呢?正常的UIViewController的初始化->界面展現(xiàn)流程是:
init -> loadView -> viewDidLoad -> viewWillApear:
調(diào)用self.view后流程是:
init(loadView -> viewDidLoad) -> viewWillAppear:
在初始化中調(diào)用self.view后,系統(tǒng)會(huì)自動(dòng)觸發(fā)loadView, viewDidLoad流程。在init方法<strong>期間</strong>會(huì)依次調(diào)用loadView -> viewDidLoad,此時(shí)初始化的數(shù)據(jù)還未完成,viewDidLoad方法很可能拿到空數(shù)據(jù)(比如上述代碼根據(jù)init初始化后的orderId來請(qǐng)求訂單相關(guān)數(shù)據(jù)),程序就會(huì)異常。除此之外,我們可能在創(chuàng)建完UIViewController后,并不是想立即展現(xiàn)它,而是希望采用懶加載在想展示時(shí),再進(jìn)行viewDidLoad過程中創(chuàng)建界面、數(shù)據(jù)處理或請(qǐng)求資源。

舉了不少例子說明不宜在初始化中使用self,那么還有方法需要注意嗎?
dealloc內(nèi)最好也別用self.property = xxx
跟初始化類似,dealloc方法也是一個(gè)過程性,“不穩(wěn)定”的方法。這里的不穩(wěn)定指的是當(dāng)前過程是一個(gè)不完全的狀態(tài),不完全初始化,不完全釋放(析構(gòu))。
dealloc除了會(huì)遇到初始化中介紹的問題以外,還經(jīng)常出現(xiàn)KVO機(jī)制引發(fā)的異常。當(dāng)一個(gè)對(duì)象A監(jiān)聽對(duì)象B的屬性C時(shí),如果在B的dealloc內(nèi)調(diào)用B.C = nil,就會(huì)觸發(fā)A中的監(jiān)聽方法。此時(shí)如果再使用B中的一些屬性或者方法,B處于半釋放狀態(tài),就會(huì)引起一些異常的奇奇怪怪的問題。所以,此時(shí)使用_C = nil更加安全。
結(jié)語
本文先分析了不建議在初始化中使用self的原因,并通過多個(gè)例子進(jìn)行證明,最后衍生出dealloc也最好別用的推論。雖然大多情況下,大家使用self沒有出錯(cuò)(在你一直都能保證調(diào)用的方法及屬性的設(shè)置不會(huì)影響其他代碼情況下),但風(fēng)險(xiǎn)就在那里,self在,它就在。
說到這里,大家很可能會(huì)想到Objective-C的繼承者---Swift類的兩段式構(gòu)造過程,它更安全、規(guī)范。Swift通過這兩個(gè)構(gòu)造過程保證了所有需要初始化的屬性都能初始化完,避免了因?qū)傩詻]有初始值導(dǎo)致之后使用過程不可預(yù)知狀況。不太清楚又想了解Swift類的兩段式構(gòu)造過程的同學(xué)可以戳官方中文教程。
最后,感謝大家的閱讀,有問題請(qǐng)指正,大家相互討論~
參考文章
Initializing a property, dot notation
Should I refer to self.property in the init method with ARC?
Objective-C init: Why It’s Helpful to Avoid Messages to self
Practical Memory Management