惰性计算辨析

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

目录
相关文章
|
29天前
|
算法
优化策略:揭秘钢条切割与饼干分发的算法艺术
本文探讨了钢条切割与饼干分发两个经典算法问题,展示了算法在解决实际问题中的应用。钢条切割问题通过动态规划方法,计算出不同长度钢条的最大盈利切割方式,考虑焊接成本后问题更为复杂。饼干分发问题则采用贪心算法,旨在尽可能多的喂饱孩子,分别讨论了每个孩子一块饼干和最多两块饼干的情况。这些问题不仅体现了数学的精妙,也展示了工程师的智慧与创造力。
37 4
|
6月前
|
运维 Serverless 数据库
函数计算产品使用问题之如何并行运算函数计算任务,并对任务计算后的结果再进行聚合运算
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
|
5月前
|
存储 前端开发 Java
理解编程语言的严格和惰性计算
【7月更文挑战第13天】本文介绍惰性计算推迟了表达式求值,直到其值真正需要时才执行,从而优化性能,节省资源。在前端和并发编程中,懒加载和类似技术结合函数式编程特性,如 continuations,平衡了抽象与性能。
85 4
理解编程语言的严格和惰性计算
|
5月前
|
搜索推荐 Java 自然语言处理
计算文本相似度的几种方法
计算文本相似度的几种方法
|
人工智能 关系型数据库 Serverless
向量加成,FC函数计算无限可能
阿里函数计算FC、文件存储NAS和RDS PostgreSQL搭建AI知识库问答应用。
122 15
|
存储
指针深度应用、解析及辨析(附代码)
指针深度应用、解析及辨析(附代码)
98 0
指针深度应用、解析及辨析(附代码)
|
数据采集 存储 监控
生活可能就像一盒巧克力,但您的数据策略不应该
罗伯特·泽米基斯(Robert Zemeckis)在1994年的电影《阿甘正传》中有一句话“生活就像一盒巧克力。你永远都不知道会得到什么。”
生活可能就像一盒巧克力,但您的数据策略不应该
|
存储 消息中间件 分布式计算
惰性函数|学习笔记
快速学习惰性函数。
176 0
惰性函数|学习笔记
|
前端开发 JavaScript
秒懂高阶编程之惰性函数
秒懂高阶编程之惰性函数
203 0
秒懂高阶编程之惰性函数
|
设计模式 缓存 前端开发
Day09 - 闭包应用1 - 制造惰性函数
Day09 - 闭包应用1 - 制造惰性函数
120 0