初始化方法内使用self有什么坏处?

初始化方法内使用self有什么坏处?

托腮思考

场景描述

iOS初始化方法包括系统默认的和自定义的,常见系统初始化方法有init, initWithFrame:, initWithNibName:bundle:等,自定义则是各式各样。日常iOS项目开发过程中,我们经常在类的初始化方法中初始化接下来类需要用到的一些必要的数据或界面。初始化方法内使用self的场景大致有两种,一是self调用方法,诸如:[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

那么,为什么不建议在初始化方法内self.property = xxx or [self doSomething]类似代码呢?

问题分析

当使用self.property = xxx时,系统会帮我们做这两件事情:

  1. 方法调用。[self setProperty:xxx]
  2. KVO。发送该属性的变化给监听者

那么,合并一下[self doSomething],初始化调用self大体就分为方法调用和KVO。这两件事情在一般情况下不会有问题,但在类在初始化的过程中,类处于一种部分初始化的状态,此时很有可能出现错误。因为执行的方法体或者监听属性值变化的对象会认为当前执行过程是完全初始化的稳定状态,当类执行体使用了还未初始化的数据时,就可能发生数据错乱,程序异常或者crash。

下面我们举例子说明~

举例佐证

为了更好地说明,如下代码中,假设我们有一个HHAnimal类,有三个属性,age年龄,name动物名字(目前是可读), attrDescription表示用于展示的带颜色的名字(只读属性),它是一个计算变量---根据年龄变化,名字的颜色不一样。

@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

我们从出现错误的概率,一级级的往上递增。现在我们希望在初始化后,attrDescription变量也被初始化。第一种添加方法 (直接在设置完默认年龄后调用[self updateAttrDescription]):

- (instancetype)init {
    self = [super init];
    if (self) {
        self.age = 12;
        [self updateAttrDescription];
        _name = @"HH";
    }
    return self;
}

有人会说,这种方法弱爆了,看我的:

- (void)setAge:(NSUInteger)age {
    _age = age;
    [self updateAttrDescription];
}
耶~

这种方式高级一些,将attrDescription跟年龄关联起来。嗯,不错。但还是crash了,因为在执行[self updateAttrDescription]时,name为nil,而[NSAttributedString initWithString:attributes:]方法调用时若string为nil,苹果爸爸直接给崩了。

那为什么你会这么写呢?其中原因之一可能是因为你不知道该系统方法不能将nil作为参数,另外一个重要的点是你很明显看到name在初始化方法中已经被赋值了,这样就不存在nil的问题。

这种显而易见的场景在日常开发过程中,我们会很快发现。但,假设有一个子类HHHuman继承了HHAnimal,它只能看到父类的.h文件(且声明name是具有初始值的)。若HHHuman的实现内重写了agesetter方法,并将name当做已初始化的一个变量使用的话,就可能引入崩溃等问题。

[UIViewController view]

除了上面介绍的一些例子,日常开发中一个更复杂也常见的例子要属UIViewController的property--view。假设在初始化的过程中写了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:...];
}

这样写,会有什么奇怪的事情发生呢?正常的UIViewController的初始化->界面展现流程是:

init -> loadView -> viewDidLoad -> viewWillApear:

调用self.view后流程是:

init(loadView -> viewDidLoad) -> viewWillAppear:

在初始化中调用self.view后,系统会自动触发loadView, viewDidLoad流程。在init方法<strong>期间</strong>会依次调用loadView -> viewDidLoad,此时初始化的数据还未完成,viewDidLoad方法很可能拿到空数据(比如上述代码根据init初始化后的orderId来请求订单相关数据),程序就会异常。除此之外,我们可能在创建完UIViewController后,并不是想立即展现它,而是希望采用懒加载在想展示时,再进行viewDidLoad过程中创建界面、数据处理或请求资源。

举了不少例子说明不宜在初始化中使用self,那么还有方法需要注意吗?

dealloc内最好也别用self.property = xxx

跟初始化类似,dealloc方法也是一个过程性,“不稳定”的方法。这里的不稳定指的是当前过程是一个不完全的状态,不完全初始化,不完全释放(析构)。

dealloc除了会遇到初始化中介绍的问题以外,还经常出现KVO机制引发的异常。当一个对象A监听对象B的属性C时,如果在B的dealloc内调用B.C = nil,就会触发A中的监听方法。此时如果再使用B中的一些属性或者方法,B处于半释放状态,就会引起一些异常的奇奇怪怪的问题。所以,此时使用_C = nil更加安全。

结语

本文先分析了不建议在初始化中使用self的原因,并通过多个例子进行证明,最后衍生出dealloc也最好别用的推论。虽然大多情况下,大家使用self没有出错(在你一直都能保证调用的方法及属性的设置不会影响其他代码情况下),但风险就在那里,self在,它就在。

说到这里,大家很可能会想到Objective-C的继承者---Swift类的两段式构造过程,它更安全、规范。Swift通过这两个构造过程保证了所有需要初始化的属性都能初始化完,避免了因属性没有初始值导致之后使用过程不可预知状况。不太清楚又想了解Swift类的两段式构造过程的同学可以戳官方中文教程

最后,感谢大家的阅读,有问题请指正,大家相互讨论~

参考文章

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容