惰性计算辨析

简介: 原文其实应该叫惰性求值(Lazy Evaluation)比较标准。就在大约一两个小时之前,有一位我博客的读者在评论区里留言,提到最近臧成威写了一篇《聊一聊iOS开发中的惰性计算》,里面提到了一个观点是除了创建非常大的属性、或者创建对象的时候有一些必要的副作用不能提前创建之外,几乎不应该使用惰性求值来处理类似逻辑。

原文
其实应该叫惰性求值(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的原因。更加具体的原因我在这篇文章里也已经说清楚了,没看过的同学可以过去看一下。

目录
相关文章
|
6月前
|
机器学习/深度学习 算法 决策智能
智能解决装箱问题:使用优化算法实现高效包装
装箱问题(Bin Packing Problem)是组合优化领域中的一个经典问题,主要涉及如何将一系列对象高效地装入有限数量的容器(或“箱”)中,同时满足特定的约束条件。这个问题的目标是最小化所需使用的箱子数量或者最大化箱子的装载效率,以减少空间或资源的浪费。
|
6月前
|
机器学习/深度学习 算法 Python
动态规划法和策略迭代在扫地机器人中确定状态值和动作值函数的策略评估(python实现 附源码 超详细)
动态规划法和策略迭代在扫地机器人中确定状态值和动作值函数的策略评估(python实现 附源码 超详细)
74 0
|
4月前
|
机器学习/深度学习 搜索推荐 数据挖掘
详解相似度计算方法及其应用场景
详解相似度计算方法及其应用场景
|
机器学习/深度学习 数据采集 搜索推荐
特征构造:从原始数据中创造出高效信息
特征构造:从原始数据中创造出高效信息
151 0
|
机器学习/深度学习 传感器 算法
【图像重建】在线全息图的迭代双图像自由重建附matlab代码
【图像重建】在线全息图的迭代双图像自由重建附matlab代码
|
算法
算法创作 | 将数字变成 0 的操作次数
算法创作 | 将数字变成 0 的操作次数
120 0
|
机器学习/深度学习 算法 搜索推荐
基于动态背包的多场景广告序列投放算法
电商广告是广告主接触其目标用户的重要手段。普遍的广告目标是在预算约束下,在一定时间范围内最大化广告主累计收入。实际应用中,广告的转化通常需要对同一用户进行多次曝光,直到该用户最终购买为止。但是,现有的广告系统主要关注单次广告曝光的直接收益,而忽略了每次曝光对最终转化的贡献,因此通常属于次优解决方案。在本文中,我们将广告序列投放策略优化转化为一个动态背包问题。为求解此背包问题,我们提出了一个具有理论保证的双层优化框架,该框架在不影响求解精度同时,显着减少了原始优化问题的求解空间。在下层框架的优化中,我们引入强化学习并设计了一种有效的动作空间约减方法,提高了强化学习在实际广告应用中的探索效率。
1847 1
基于动态背包的多场景广告序列投放算法
使用scanpy进行高可变基因的筛选
使用scanpy进行高可变基因的筛选
|
机器学习/深度学习 人工智能 算法
整个元素周期表通用,AI 即时预测材料结构与特性
整个元素周期表通用,AI 即时预测材料结构与特性
171 0
|
机器学习/深度学习 自然语言处理 达摩院
Span抽取和元学习能碰撞出怎样的新火花,小样本实体识别来告诉你!
这是一种面向命名实体识别的小样本学习算法,采用两阶段的训练方法,检测文本中最有可能是命名实体的Span,并且准确判断其实体类型,在仅需要标注极少训练数据的情况下,提升预训练语言模型在命名实体识别任务上的精度。