聊一聊 iOS 开发中的惰性计算

本文摘自:作者臧成威,美团网 iOS 技术专家,QCon 讲师,国内 Functional Reactive Programming 技术爱好者.2015年加入美团,负责美团 iOS 发布工程系统的研发和流程优化梳理。擅长多语言范式,对各种编程范式有着独到的见解.

臧老师在美团组织过系统的 Functional Reactive Programming 培训,参与人数总计达百人。

他最近在 InfoQ 旗下的 StuQ 开设课程:《ReactiveCocoa 编程思想与开发实战》,第一期爆满结束。于是他最近开了第二期课程,本周五即将上课,感兴趣的可以看文末的课程详情。

正文

首先给大家讲一个笑话:

有一只小白兔,跑到蔬菜店里问老板:“老板,有 100 个胡萝卜吗?”。老板说:“没有那么多啊。”,小白兔失望的说道:“哎,连 100 个胡萝卜都没有。。。”。

第二天小白兔又来到蔬菜店问老板:“今天有 100 个胡萝卜了吧?”,老板尴尬的说:“今天还是缺点,明天就能好了。”,小白兔又很失望的走了。

第三天小白兔刚一推门,老板就高兴的说道:“有了有了,从前天就进货的 100 个胡萝卜到货了。”,小白兔说:“太好了,我要买 2 根!”。。。

不晓得笑话是否博您一笑,但是这里面确有一个点是和我们的主题惰性计算相关的。试想一下,假设蔬菜店是一个电商,你是老板,你挂商品数量的时候,是 100 个,1000 个,还是真实的备货 2 个?显然做过淘宝的同学都知道这其中的玄机,就是先挂大的余量,有卖出再补货。所以,如果这个老板先回答有 100 个胡萝卜,再等它要 2 个的时候把自己备货的 2 个拿给它,是不是就免去了 100 个胡萝卜的物流?

在程序开发中,我们也会经常的遇到这样的问题,明明创建了很大的一个对象,但是其实只用了一个字段;明明创建了一个 500 个的数组,其实只用了第 0 个和第 1 个元素。遇到这类问题,我们可以尝试使用惰性计算来解决。

关于惰性计算,或者惰性求值。想必大家第一反应就是在 getter 里动态返回属性了。例如有一个很大的属性,你希望在有人调用的时候才创建,就可以这样写:

- (id)someBigProperty

{

if (_someBigProperty == nil) {

NSMutableArray *someBigProperty = [NSMutableArray array];

for (int i = 0; i < 100000; ++i) {

[someBigProperty addObject:@(i)];

}

_someBigProperty = [someBigProperty copy];

}

return _someBigProperty;

}

本文当然不拘泥于大家耳熟能详的知识点进行阐述了。上述的代码虽然也能勉强叫惰性求值,但并非足够理想。为什么说是 “勉强叫” 呢?大家想想上面的笑话,其实这样做和老板的做法并无差别。首先店里没有 100 个胡萝卜,就好像这个对象没有_someBigProperty属性一样。一旦有人需要 100 个 “胡萝卜”,就循环 100000 次创建这个_someBigProperty属性。然后可能使用者只需要第 0 个。

另外在实际项目中这样的一个手段几乎被大家严重的乱用了,为什么说是乱用呢?除了创建非常大的属性、或者创建对象的时候有一些必要的副作用不能提前创建之外,几乎不应该使用惰性求值来处理类似逻辑。原因如下:

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

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

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

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

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

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

我们回到正题。既然简单改写一下 getter 不但解决不了问题还有这么多隐患,那我们该如何能够正确优雅的把惰性计算写好?下面给大家一些建议。

观察上面的代码,你会发现 _someBigProperty 是一个非常规则的 NSArray,它的 item 内容与下标相等。我们可以看出 item 的结果与 index 存在如下关系:

f(x) = x

类似的可以有很多,例如> 100的为@“world”,0 <= x <= 100的为@“hello”;item 为下标的平方;item 为下标的数值转换成的字符串等。所以这类NSArray,基本需要一个 count 和一个函数就可以构成了。那我们现在就基于NSArray这个类簇,实现一个特殊的类吧!

关于类簇,相信很多同学都有所了解,大概的说法是不可以直接继承一个NSArray、NSNumber、NSString这样的类。如果要继承需要实现全部的必要方法,在NSArray这个类簇来说,就是如下的方法:

@interface NSArray<__covariant ObjectType> : NSObject

@property (readonly) NSUInteger count;

- (ObjectType)objectAtIndex:(NSUInteger)index;

- (instancetype)init NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithObjects:(const ObjectType [])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

@end

当然除了NSArray类的基本方法,还有NSCopying、NSMutableCopying、NSSecureCoding这些协议需要实现,另外NSFastEnumberation协议已经默认实现完成,不需要额外处理。与惰性计算无关的细节大家可以自己填补,对于本例,我们只需要关心这几个方法的实现:

typedef id(^ItemBlock)(NSUInteger index);

@interface ZDynamicArray : NSArray

- (instancetype)initWithItemBlock:(ItemBlock)block count:(NSUInteger)cnt;

- (id)objectAtIndex:(NSUInteger)index;

- (NSUInteger)count;

@end

按照上文的说法,对于这样一个特殊的NSArray,我们真正要储存的数据只有一个 count 值外加一个函数,所以我们用这两个作为init参数。实现也很简单:

@interface ZDynamicArray()

@property (nonatomic, readonly) ItemBlock block;

@property (nonatomic, readonly) NSUInteger cnt;

@end

@implementation ZDynamicArray

- (instancetype)initWithItemBlock:(ItemBlock)block count:(NSUInteger)cnt

{

if (self = [super init]) {

_block = block;

_cnt = cnt;

}

return self;

}

- (NSUInteger)count

{

return self.cnt;

}

- (id)objectAtIndex:(NSUInteger)index

{

if (self.block) {

return self.block(index);

} else {

return nil;

}

}

@end

瞧,就这么简单的写好了。让我们试一下吧!

ZDynamicArray *array = [[ZDynamicArray alloc] initWithItemBlock:^id(NSUInteger index) {

return @(index);

} count:100000];

for (id v in array) {

NSLog(@"%@", v);

}

NSLog(@"%@", array[15]);

一个看似 10w 数据的数组,其实占用空间微乎其微,但是作用和最开始那样的代码效果一样。很不错吧。大家也可以动手实践,写一些自己需要用到的惰性计算代码,例如一个Model的数组,并非所有的Model都需要用到,我们也可以做成这样的一个数组,等用到的时候再从NSDicitonary转换成Model。就像这样:

NSArray *downloadData = @[@{}, @{}, @{}, @{}];

NSArray *modelArray = [[ZDynamicArray alloc] initWithItemBlock:^id(NSUInteger index) {

return [SomeModel modelFromDictionary:downloadData[index]];

} count:downloadData.count];

当然这可能有一定的风险,因为传统的写法会更早一步的发现某些数据不正确,然后惰性计算,会把这个发现问题的时间延后。这就需要更多更好的错误处理机制。ReactiveCocoa这个著名的 FRP 库为我们提供了更多编程的可能,它在很多处理上都是惰性计算的,同时它又做了很好的异常处理工作。学习它可以让你编程思路更广。

全文完。

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

推荐阅读更多精彩内容