本文摘自:作者臧成威,美團網(wǎng) iOS 技術(shù)專家蜻牢,QCon 講師盾饮,國內(nèi) Functional Reactive Programming 技術(shù)愛好者.2015年加入美團,負責美團 iOS 發(fā)布工程系統(tǒng)的研發(fā)和流程優(yōu)化梳理汞斧。擅長多語言范式生真,對各種編程范式有著獨到的見解.
臧老師在美團組織過系統(tǒng)的 Functional Reactive Programming 培訓(xùn),參與人數(shù)總計達百人吏祸。
他最近在 InfoQ 旗下的 StuQ 開設(shè)課程:《ReactiveCocoa 編程思想與開發(fā)實戰(zhàn)》对蒲,第一期爆滿結(jié)束。于是他最近開了第二期課程,本周五即將上課齐蔽,感興趣的可以看文末的課程詳情两疚。
正文
首先給大家講一個笑話:
有一只小白兔,跑到蔬菜店里問老板:“老板含滴,有 100 個胡蘿卜嗎诱渤?”。老板說:“沒有那么多啊谈况∩酌溃”,小白兔失望的說道:“哎碑韵,連 100 個胡蘿卜都沒有赡茸。。祝闻≌嘉裕”。
第二天小白兔又來到蔬菜店問老板:“今天有 100 個胡蘿卜了吧联喘?”华蜒,老板尷尬的說:“今天還是缺點,明天就能好了豁遭“认玻”,小白兔又很失望的走了蓖谢。
第三天小白兔剛一推門捂蕴,老板就高興的說道:“有了有了,從前天就進貨的 100 個胡蘿卜到貨了闪幽∩侗妫”,小白兔說:“太好了沟使,我要買 2 根委可!”。腊嗡。。
不曉得笑話是否博您一笑拾酝,但是這里面確有一個點是和我們的主題惰性計算相關(guān)的燕少。試想一下,假設(shè)蔬菜店是一個電商蒿囤,你是老板客们,你掛商品數(shù)量的時候,是 100 個,1000 個底挫,還是真實的備貨 2 個恒傻?顯然做過淘寶的同學都知道這其中的玄機,就是先掛大的余量建邓,有賣出再補貨盈厘。所以,如果這個老板先回答有 100 個胡蘿卜官边,再等它要 2 個的時候把自己備貨的 2 個拿給它沸手,是不是就免去了 100 個胡蘿卜的物流?
在程序開發(fā)中注簿,我們也會經(jīng)常的遇到這樣的問題契吉,明明創(chuàng)建了很大的一個對象,但是其實只用了一個字段诡渴;明明創(chuàng)建了一個 500 個的數(shù)組捐晶,其實只用了第 0 個和第 1 個元素。遇到這類問題妄辩,我們可以嘗試使用惰性計算來解決惑灵。
關(guān)于惰性計算,或者惰性求值恩袱。想必大家第一反應(yīng)就是在 getter 里動態(tài)返回屬性了泣棋。例如有一個很大的屬性,你希望在有人調(diào)用的時候才創(chuàng)建畔塔,就可以這樣寫:
- (id)someBigProperty
{
if (_someBigProperty == nil) {
NSMutableArray *someBigProperty = [NSMutableArray array];
for (int i = 0; i < 100000; ++i) {
[someBigProperty addObject:@(i)];
}
_someBigProperty = [someBigProperty copy];
}
return _someBigProperty;
}
本文當然不拘泥于大家耳熟能詳?shù)闹R點進行闡述了潭辈。上述的代碼雖然也能勉強叫惰性求值,但并非足夠理想澈吨。為什么說是 “勉強叫” 呢把敢?大家想想上面的笑話,其實這樣做和老板的做法并無差別谅辣。首先店里沒有 100 個胡蘿卜修赞,就好像這個對象沒有_someBigProperty屬性一樣。一旦有人需要 100 個 “胡蘿卜”桑阶,就循環(huán) 100000 次創(chuàng)建這個_someBigProperty屬性柏副。然后可能使用者只需要第 0 個。
另外在實際項目中這樣的一個手段幾乎被大家嚴重的亂用了蚣录,為什么說是亂用呢割择?除了創(chuàng)建非常大的屬性、或者創(chuàng)建對象的時候有一些必要的副作用不能提前創(chuàng)建之外萎河,幾乎不應(yīng)該使用惰性求值來處理類似邏輯荔泳。原因如下:
如果真的是很大的屬性蕉饼,一般它比較重要,幾乎一定會被訪問玛歌,所以加上這個不如直接在 init 的時候創(chuàng)建昧港。
@property 的 atomic、nonatomic支子、copy创肥、strong 等描述在有 getter 方法的屬性上會失效,后人修改代碼的時候可能只改了 @property 聲明译荞,并不會記得改 getter瓤的,于是隱患就這樣埋下了。
代碼含有了隱私操作吞歼,尤其 getter 中再混雜了各種邏輯圈膏,使得程序出現(xiàn)問題非常不好排查。后人哪會想到someObj.someProperty這樣一個簡簡單單的取屬性發(fā)生了很多奇妙的事篙骡。
很多人的 getter 寫得并不是完全標準稽坤,例如上述代碼會導(dǎo)致多線程訪問的時候,出現(xiàn)很多神奇的問題糯俗。一旦形成習慣尿褪,后續(xù)的很多稀奇古怪的 crash 就接踵而至了。
代碼多得湘,本來代碼只需要在init方法中創(chuàng)建用上一兩行杖玲,結(jié)果用了至少 7 行的一個 getter 方法才能寫出來,想想一個程序輕則數(shù)百個屬性淘正,都這么搞摆马,得多出多少行代碼?另外代碼格式幾乎完全一樣鸿吆,不符合 DRY 原則囤采。好的程序員不應(yīng)該總是寫重復(fù)的代碼,不是么惩淳?
性能損耗蕉毯,對于屬性取值可能會非常的頻繁,如果所有的屬性取值之前都經(jīng)過一個if判斷思犁,這不是平白浪費的性能代虾?
我們回到正題。既然簡單改寫一下 getter 不但解決不了問題還有這么多隱患激蹲,那我們該如何能夠正確優(yōu)雅的把惰性計算寫好褐着?下面給大家一些建議。
觀察上面的代碼托呕,你會發(fā)現(xiàn) _someBigProperty 是一個非常規(guī)則的 NSArray含蓉,它的 item 內(nèi)容與下標相等。我們可以看出 item 的結(jié)果與 index 存在如下關(guān)系:
f(x) = x
類似的可以有很多项郊,例如> 100的為@“world”馅扣,0 <= x <= 100的為@“hello”;item 為下標的平方着降;item 為下標的數(shù)值轉(zhuǎn)換成的字符串等差油。所以這類NSArray,基本需要一個 count 和一個函數(shù)就可以構(gòu)成了任洞。那我們現(xiàn)在就基于NSArray這個類簇蓄喇,實現(xiàn)一個特殊的類吧!
關(guān)于類簇交掏,相信很多同學都有所了解妆偏,大概的說法是不可以直接繼承一個NSArray、NSNumber盅弛、NSString這樣的類钱骂。如果要繼承需要實現(xiàn)全部的必要方法,在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這些協(xié)議需要實現(xiàn)解取,另外NSFastEnumberation協(xié)議已經(jīng)默認實現(xiàn)完成,不需要額外處理返顺。與惰性計算無關(guān)的細節(jié)大家可以自己填補禀苦,對于本例,我們只需要關(guān)心這幾個方法的實現(xiàn):
typedef id(^ItemBlock)(NSUInteger index);
@interface ZDynamicArray : NSArray
- (instancetype)initWithItemBlock:(ItemBlock)block count:(NSUInteger)cnt;
- (id)objectAtIndex:(NSUInteger)index;
- (NSUInteger)count;
@end
按照上文的說法创南,對于這樣一個特殊的NSArray伦忠,我們真正要儲存的數(shù)據(jù)只有一個 count 值外加一個函數(shù),所以我們用這兩個作為init參數(shù)稿辙。實現(xiàn)也很簡單:
@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 數(shù)據(jù)的數(shù)組赋咽,其實占用空間微乎其微,但是作用和最開始那樣的代碼效果一樣吨娜。很不錯吧脓匿。大家也可以動手實踐,寫一些自己需要用到的惰性計算代碼宦赠,例如一個Model的數(shù)組陪毡,并非所有的Model都需要用到米母,我們也可以做成這樣的一個數(shù)組,等用到的時候再從NSDicitonary轉(zhuǎn)換成Model毡琉。就像這樣:
NSArray *downloadData = @[@{}, @{}, @{}, @{}];
NSArray *modelArray = [[ZDynamicArray alloc] initWithItemBlock:^id(NSUInteger index) {
return [SomeModel modelFromDictionary:downloadData[index]];
} count:downloadData.count];
當然這可能有一定的風險铁瞒,因為傳統(tǒng)的寫法會更早一步的發(fā)現(xiàn)某些數(shù)據(jù)不正確,然后惰性計算桅滋,會把這個發(fā)現(xiàn)問題的時間延后慧耍。這就需要更多更好的錯誤處理機制。ReactiveCocoa這個著名的 FRP 庫為我們提供了更多編程的可能丐谋,它在很多處理上都是惰性計算的芍碧,同時它又做了很好的異常處理工作。學習它可以讓你編程思路更廣号俐。
全文完泌豆。