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

初始化方法內(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ì)幫我們做這兩件事情:

  1. 方法調(diào)用。[self setProperty:xxx]
  2. 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)重寫了agesetter方法,并將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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容