Date Tags thought


其实应该叫惰性求值(Lazy Evaluation)比较标准。






就在大约一两个小时之前,有一位我博客的读者在评论区里留言,提到最近臧成威写了一篇《聊一聊iOS开发中的惰性计算》,里面提到了一个观点是除了创建非常大的属性、或者创建对象的时候有一些必要的副作用不能提前创建之外,几乎不应该使用惰性求值来处理类似逻辑。。并且主要给出了六点理由。他给出的结论和我提倡的做法相悖,问我是什么看法。


在这里我先离题一下:我完整地看完了这篇文章,很喜欢这种立场鲜明,而且有清晰理由的文章。我先不说理由是否合理,立场是否正确,至少我看到国内技术圈子里的大多数文章其实没有任何观点和立场,都只是教你XXX怎么用,而且写得又不比官方文档好,含金量很低。即便在少数有观点的文章中,大部分又只有观点,没有任何理由。臧成威这篇文章是逻辑清晰,有观点而且有理由的,这篇文章在这一点上其实是做的很好的。


那么,接下来我就要在这篇文章中辨析一下他的文章里提供的六个理由了。






如果真的是很大的属性,一般它比较重要,几乎一定会被访问,所以加上这个不如直接在 init 的时候创建。


这个理由其实是有逻辑问题的。


虽然这句话并没有很绝对地说很大的属性就一定比较重要,给你造成一种看起来说得很客观,很有道理的假象。更何况,事实上一个属性的重要程度其实是和属性本身的大小也是无关的。


但另外一点是,惰性求值并不影响属性的可访问性,即使前面属性很大属性很重要属性一定会被访问都满足,我实在看不出这三个条件能够给出在init的时候创建的倾向。惰性求值和及早求值的差别完全不在那三点前提,这里的理由和给出的结论其实是完全无关的。


这句话其实就是类似这样的句子:因为西瓜很大,所以西瓜一般比较重要,而且夏天基本上一定都会吃西瓜,所以西瓜还不如直接用小卡车运进城,就不要用拖拉机了。 我再离题一下:这种话术其实很有欺骗性,我们国家也经常采用这种话术来欺骗百姓,不过这里我们不谈国事,你懂的。


总结来说就是,臧成威的这条理由根本无法去支撑他的论点,这本质上并不是技术问题,是思维逻辑问题,我不去揣测臧成威的动机是故意还是无意的,我只指出这逻辑是错的。




@property 的 atomic、nonatomic、copy、strong 等描述在有 getter 方法的属性上会失效,后人修改代码的时候可能只改了 @property 声明,并不会记得改 getter,于是隐患就这样埋下了。


我当时看到这个理由的时候我非常吃惊,除了atomic和nonatomic以外,其它的其实都是修饰setter的啊,为什么用了getter就失效了?这不合常识啊。于是我做了求证:


首先,Strong/Weak 在getter中编译器是会warning的,从编译器的warning上看,谈不上失效。看下面的例子:


@interface ViewController ()

@property (nonatomic, weak) NSArray *testArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"%@", self.testArray);
}

#pragma mark - getters and setters
- (NSArray *)testArray
{
    if (_testArray == nil) {
        _testArray = [[NSArray alloc] init]; // 此处会报warning: Assigning retained object to weak variable; object will be released after assignment.
    }
    return _testArray;
}

@end




然后,copy在有getter方法的属性上也不会失效,因为copy完全修饰的是setter方法,与getter无关。看下面的例子:


@interface ViewController ()

@property (nonatomic, copy) NSArray *testArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSMutableArray *aArray = [[NSMutableArray alloc] initWithObjects:@123, nil];
    self.testArray = aArray;

    [aArray addObject:@456];

    NSLog(@"%@", aArray); // 输出123,456
    NSLog(@"%@", self.testArray); // 输出123,而不是123,456。证明copy并没有失效,如果copy失效,那应该也输出123,456。
}

#pragma mark - getters and setters
- (NSArray *)testArray
{
    if (_testArray == nil) {
        _testArray = [[NSArray alloc] init];
    }
    return _testArray;
}

@end




最后是nonatomic和atomic,这个我现在并不知道有什么比较好的手段去求证这个问题,我现在比较忙并没有时间去查资料。但针对这个情况我要说两点:


  1. 实际开发工作中基本上都是用的nonatomic去修饰一个property,如果真的要进行原子操作,往往是自己用锁来建立临界区,很少情况是用atomic。原因见第二条:
  2. 因为atomic并不能保证线程安全,线程安全应当由工程师自己通过锁来建立临界区。我记得这个描述苹果官方文档有说过,并且举了一个Person对象的例子。实际出处的链接我一时半会儿找不到了,大家如果有空的话可以帮我找一下。


针对第二条有网友补充:在多个atomic property的情况下,atomic并不能保证他们取值赋值的时序,因此不能保证线程安全。但对于单个property而言,atomic是安全的。在实际工作中,往往临界区涉及的属性和数据并不惟一,因此实际开发场景都是推荐工程师自建临界区,另一个角度上,这也方便将来增加或删除临界区相关的变量。


所以如果要自建临界区的话,其实用getter只会比不用getter更好,因为临界区里面涉及的逻辑和变量有可能很复杂,而我们并不希望这部分复杂的代码泄漏到与之无关的主要逻辑中去,这样会使得主要逻辑不清晰,难以维护。




代码含有了隐私操作,尤其 getter 中再混杂了各种逻辑,使得程序出现问题非常不好排查。后人哪会想到someObj.someProperty这样一个简简单单的取属性发生了很多奇妙的事。


是否要在getter中写逻辑,这其实是一个主观问题。


如果你决定要在getter中写逻辑,那么就应当只写跟初始化过程有关的逻辑,跟初始化过程无关的逻辑就不要在getter里面写。因为getter本质上其实是工厂方法,工厂方法是不应当跟业务掺杂过多的。


实际开发过程中,确实有人把不必要的逻辑写进getter中,这些都是我在code review的过程会打回让他重写的。一般新人进我的team都会有一个月的code review过程来进行教育,所以乱写的情况很少。


最后,这本质上是一个主观问题并不是一个客观技术问题,更不属于getter的技术缺陷,真要说技术缺陷的话,上面一条更加类似。而且,对于一个傻逼来说,不管使不使用getter,他都一样会给你写出难以排查问题的代码。所以真正要做的事情是把傻逼教好,而不是不使用getter。


这叫因噎废食,傻逼不会吃饭噎到自己了,不去考虑怎么学习吃饭的正确方法,反而决定不吃饭了。




很多人的 getter 写得并不是完全标准,例如上述代码会导致多线程访问的时候,出现很多神奇的问题。一旦形成习惯,后续的很多稀奇古怪的 crash 就接踵而至了。


这个结论其实并没有详细的理由去支撑。神奇的问题具体是什么?跟getter有关吗?在我做过的项目中,由于使用了getter,排查问题时就能够非常有目的性,只要先搞清楚是变量初始化的问题,还是逻辑操作的流程问题,就基本上能够很快定位到问题点了。


这种模糊表达的话术,其实就是典型的当年秦桧的莫须有。嗯,有crash出来了,而且莫名其妙,所以,可能就是getter导致的吧?这个还真难反驳,但如果臧成威你遇到了这个神奇的问题,且与getter有关,那就列举出来,然后我们再来就这个理由继续讨论。在此之前,这个锅getter表示不背。


至于getter写得不标准,其实我在上一条里面已经说清楚了:即使你不写getter,傻逼们一样会给你搞出各种莫名其妙的crash,相信你即使不去blame getter,也会去blame 其它。




代码多,本来代码只需要在init方法中创建用上一两行,结果用了至少 7 行的一个 getter 方法才能写出来,想想一个程序轻则数百个属性,都这么搞,得多出多少行代码?另外代码格式几乎完全一样,不符合 DRY 原则。好的程序员不应该总是写重复的代码,不是么?


其实这个问题其实是这样的,使用getter和不使用getter,在代码行数上的差别仅多出5行,剩下的其实都一样。


然后一个程序轻则数百个属性,这个我是不认可的,一个程序里面,20-30个属性已经算是非常大了,我真从来没见过有哪个对象有数百个属性的,如果真的存在,那说明这个工程的模块划分、对象划分存在问题,这是一个比使用和不使用getter都更加严重的问题。即使你不使用getter,如果遇到了数百属性的对象,首先要做的事情也必须是重新考虑模块划分和对象划分。而且再退一步说,一个对象中也不是每个属性都要有getter的。


臧成威的这种说法其实是潜移默化地在扩大范围,如果所有XXX都这么搞,那得XXX?这个话术其实就是在夸大问题范围,以前别人写过一篇文章讨论过这种话术,不过我现在也找不到出处了。


而且,就这个问题来看,使用getter的好处十分明显,一个程序的初始化区域和逻辑执行区域被分隔开了,这样就能使得即使很多行代码的文件,其代码分配结构就会变得非常清晰。所以即使在非常大的对象里,使用getter来划分代码在文件中的组织结构,是非常有利于大对象维护的。


最后,关于DRY,臧成威你是不是不知道XCode有自定义的code snippt功能?我觉得至少你应该在写GCD相关代码的时候也用过吧?谈何重复?




性能损耗,对于属性取值可能会非常的频繁,如果所有的属性取值之前都经过一个if判断,这不是平白浪费的性能?


这里的性能损耗其实是一个权衡问题,也是使用惰性求值和及早求值的主要差别之一。在不使用惰性求值的时候,程序的内存foot print会因为一个对象的初始化而形成一个陡峭的曲线。使用惰性求值的好处在于能够避免不必要的内存占用。在整个程序的生命周期上,能够提高内存的使用效率,在生命周期中的某个时间维度上,可以保证后续逻辑的高效完成。


举个例子,一条逻辑分别有A,B,C三项任务构成,分别需要使用a,b,c三个属性。假设内存一次只能装得下三个属性中的任意两个,如果不使用惰性计算,这个程序的内存使用效率就非常低,不得不走swap。但如果使用了惰性计算,就完全不必去走swap来解决内存不够的问题。


相比于内存的使用效率,以及由于过大内存导致的swap所消耗的时间,这两者的性能损耗跟单纯一个if判断相比,实在是微不足道。


脱离剂量谈毒性是不对的,再退一步说,单纯多的一步if判断消耗的时间是纳秒级别,而且差不多只是两位数的纳秒,微乎其微。但是因此带来内存使用效率的提高,却是非常显著的,因此从性能角度来说,这个权衡应该更加偏向使用惰性求值才对。




结论


所以结论已经很明显了,六个理由对于getter来说其实根本站不住脚,而且使用getter的好处一方面带来了文件中更清晰的代码分布,另一方面提高了内存的使用效率。这也是为什么我推荐使用getter的原因。更加具体的原因我在这篇文章里也已经说清楚了,没看过的同学可以过去看一下。








评论系统我用的是Disqus,不定期被墙。所以如果你看到文章下面没有加载出评论列表,翻个墙就有了。




本文遵守CC-BY。

请保持转载后文章内容的完整,以及文章出处。本人保留所有版权相关权利

如果您觉得文章有价值,可以通过支付宝扫描下面的二维码捐助我。


Comments

comments powered by Disqus