NSFastEnumeration
前言
最近在實(shí)現(xiàn)一個(gè)自定義的容器類,在實(shí)現(xiàn)過程中,突然想到慕匠,如果別人在遍歷這個(gè)容器類的時(shí)候杭朱,如果可以實(shí)現(xiàn) for...in 的話阅仔,就可以讓我們的容器類遍歷起來更加的優(yōu)雅(真的么?弧械?八酒?總覺得這個(gè)是我的強(qiáng)迫癥犯了)。那么怎么樣才能讓我們的容器支持 for...in 呢刃唐?做過 iOS 同學(xué)大多數(shù)都應(yīng)該知道或者聽說過羞迷, for...in 只要實(shí)現(xiàn)一個(gè)協(xié)議即可,這個(gè)協(xié)議就是 NSFastEnumeration 画饥。定義如下:
/*
A protocol that objects adopt to support fast enumeration.
@Discussion The abstract class NSEnumerator provides a convenience implementation that uses nextObject to return items one at a time.
*/
@protocol NSFastEnumeration
/*
Returns by reference a C array of objects over which the sender should iterate, and as the return value the number of objects in the array.
@Discussion The state structure is assumed to be of stack local memory, so you can recast the passed in state structure to one more suitable for your iteration.
@param state Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.
@param buffer A C array of objects over which the sender is to iterate.
@param len The maximum number of objects to return in stackbuf.
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
@end
乍一看衔瓮,easy!就一個(gè)方法抖甘,只要我們正確的實(shí)現(xiàn)了這個(gè)方法热鞍,我們的容器類就可以支持 for...in 遍歷了。但是仔細(xì)一看各個(gè)參數(shù):
NSFastEnumerationState:
/*
This defines the structure used as contextual information in the NSFastEnumeration protocol.
*/
typedef struct {
unsigned long state; /* Arbitrary state information used by the iterator. Typically this is set to 0 at the beginning of the iteration. */
id __unsafe_unretained _Nullable * _Nullable itemsPtr; /* A C array of objects. */
unsigned long * _Nullable mutationsPtr; /* Arbitrary state information used to detect whether the collection has been mutated. */
unsigned long extra[5]; /* A C array that you can use to hold returned values. */
} NSFastEnumerationState;
WTF? 這都是些什么東西衔彻,在習(xí)慣了 ARC 之后薇宠,經(jīng)常在 OC 面向?qū)ο笾芯幊痰奈乙荒樸卤疲?strong>state 是一個(gè)結(jié)構(gòu)體,buffer 是一個(gè)指針數(shù)組米奸,這些都怎么用昼接?本文接下來會(huì)詳細(xì)的講解 NSFastEnumeration 這個(gè)協(xié)議,并且會(huì)解釋遇到的坑悴晰,最終提供一個(gè)可以執(zhí)行的慢睡,簡(jiǎn)單的 demo 容器類。
NSFastEnumeration 協(xié)議解析
回到正題铡溪,我們來仔細(xì)的看看官方給出關(guān)于 NSFastEnumeration 的注釋漂辐。下面給出我粗糙的翻譯(英文不好,請(qǐng)見諒):
/*
A protocol that objects adopt to support fast enumeration.
一個(gè)對(duì)象需要實(shí)現(xiàn)以支持快速枚舉的協(xié)議棕硫。
@Discussion The abstract class NSEnumerator provides a convenience implementation that uses nextObject to return items one at a time.
抽象的 NSEnumerator 類髓涯,提供了使用 nextObject 在同一時(shí)間返回對(duì)象的便利實(shí)現(xiàn)。
*/
@protocol NSFastEnumeration
/*
Returns by reference a C array of objects over which the sender should iterate, and as the return value the number of objects in the array.
譯:通過 C 對(duì)象數(shù)組的引用來返回給調(diào)用者需要枚舉的內(nèi)容(調(diào)用者枚舉的是一個(gè) C 數(shù)組)哈扮,并且通過方法的返回值來返回 C 對(duì)象數(shù)組中對(duì)象的個(gè)數(shù)纬纪。
注:返回兩個(gè)東西蚓再,一個(gè)是 C 對(duì)象數(shù)組,用來給調(diào)用者遍歷包各,另一個(gè)數(shù) C 對(duì)象數(shù)組的個(gè)數(shù)(C 數(shù)組摘仅,不能使用 sizeof 來取個(gè)數(shù),因?yàn)橛锌赡苁且粋€(gè)指針问畅,所以需要我們返回)娃属。
@Discussion The state structure is assumed to be of stack local memory, so you can recast the passed in state structure to one more suitable for your iteration.
譯:state 結(jié)構(gòu)體被假定為棧的本地內(nèi)存(隨棧幀銷毀,無需管理內(nèi)存)护姆,所以您可以根據(jù)您的迭代狀態(tài)矾端,重鑄(自己玩)入?yún)⒌?state 結(jié)構(gòu)體。
注:state 是棧內(nèi)存卵皂,可以根據(jù)自己容器內(nèi)部的代碼邏輯秩铆,修改 state。
@param state Context information that is used in the enumeration to, in addition to other possibilities, ensure that the collection has not been mutated.
譯:用于枚舉的上下文信息渐裂,在此之上豺旬,保證容器不會(huì)被修改。
注:state 是一個(gè)枚舉的上下文柒凉,并且在枚舉的過程中族阅,需要保證容器不會(huì)被修改。相信大家在開始寫碼不久的時(shí)候膝捞,也犯過類似的錯(cuò)誤坦刀,一邊遍歷數(shù)組,一邊修改數(shù)組蔬咬,然后出了 crash鲤遥。如果你沒有,那么... 好吧林艘,你贏了盖奈。
@param buffer A C array of objects over which the sender is to iterate.
譯:一個(gè)調(diào)用者用于迭代的 C 對(duì)象數(shù)組。
@param len The maximum number of objects to return in buffer.
譯:buffer 中對(duì)象的 最大 個(gè)數(shù)狐援。
注:用到了 maximum number 钢坦,說明 buffer 可能是不滿的,比如 buffer 可以容納 10 個(gè)對(duì)象啥酱,但是實(shí)際上可能只有 3 個(gè)對(duì)象爹凹。
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
@end
/*
This defines the structure used as contextual information in the NSFastEnumeration protocol.
譯:NSFastEnumeration 協(xié)議中的上下文信息結(jié)構(gòu)體定義。
注:廢話镶殷。
*/
typedef struct {
unsigned long state; /* Arbitrary state information used by the iterator. Typically this is set to 0 at the beginning of the iteration. */
譯:迭代器使用的任意狀態(tài)信息禾酱。通常在迭代開始時(shí),置為 0。
注:任意的狀態(tài)信息颤陶,其實(shí)就是我們自己定義的颗管,通常在迭代開始時(shí)置為 0.
id __unsafe_unretained _Nullable * _Nullable itemsPtr; /* A C array of objects. */
譯:一個(gè) C 對(duì)象數(shù)組。
unsigned long * _Nullable mutationsPtr; /* Arbitrary state information used to detect whether the collection has been mutated. */
譯:用于迭代器監(jiān)測(cè)發(fā)生修改的任意狀態(tài)信息指郁。
注:我們自己定義忙上,隨意的一個(gè)值,只是用來判斷在迭代過程中闲坎,容器是否被修改。
unsigned long extra[5]; /* A C array that you can use to hold returned values. */
譯:一個(gè)您可以用來保存返回值的 C 數(shù)組茬斧。
注:可以用腰懂,當(dāng)然也可以不用。
} NSFastEnumerationState;
經(jīng)過粗糙的翻譯项秉,現(xiàn)在大概明白了這個(gè)協(xié)議以及協(xié)議中唯一的方法的大致用法:在我們使用 for...in 去遍歷一個(gè)容器的時(shí)候绣溜,系統(tǒng)會(huì)調(diào)用這個(gè)方法,來返回需要迭代的 C 對(duì)象數(shù)組娄蔼。但是大致明白不代表明白了怖喻,怎么返回,通過 buffer 參數(shù)岁诉?state 參數(shù)是怎么用的锚沸?本人的體驗(yàn):知道了這個(gè)方法是干啥的,但是怎么用的涕癣,完全不明白哗蜈。
尋找一個(gè) NSFastEnumeration 的 demo
在經(jīng)過上述代碼描述,的確是不知道這個(gè)東西到底是怎么用的坠韩,怎么辦呢距潘?Google 唄,這種問題肯定有人遇到過只搁。然后找到 Mike Ash 的一篇文章(相信大家應(yīng)該也看到過這篇):
讀完了這篇文章之后音比,好像明白了很多贷币,也大概明白了 for...in 的實(shí)現(xiàn)苛萎。我們自己來操作一遍。
- 以 NSArray 為例近迁,寫一個(gè) for...in 的 demo
int main(int argc, const char * _Nullable argv[])
{
/// 為了 rewrite 之后方便尋找明肮,起了一個(gè)特殊的名字
NSArray *chris___array = @[@0, @1, @2];
for (id obj in chris___array) {
NSLog(@"%@", obj);
}
return 0;
}
- clang -rewrite-objc main.m(如果有不知道這個(gè)怎么用的菱农,自行百度就好,這里隨便貼一個(gè)鏈接:iOS clang -rewrite-objc)柿估,由于代碼太長(zhǎng)循未,這里只粘貼關(guān)鍵部分:
由 clang rewrite-objc 生成的關(guān)鍵 cpp 代碼:
int main(int argc, const char * _Nullable argv[])
{
NSArray *chris___array = ((NSArray *(*)(Class, SEL, ObjectType _Nonnull const * _Nonnull, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(3U, ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 0), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2)).arr, 3U);
{
id obj;
struct __objcFastEnumerationState enumState = { 0 };
id __rw_items[16];
id l_collection = (id) chris___array;
_WIN_NSUInteger limit =
((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16);
if (limit) {
unsigned long startMutations = *enumState.mutationsPtr;
do {
unsigned long counter = 0;
do {
if (startMutations != *enumState.mutationsPtr)
objc_enumerationMutation(l_collection);
obj = (id)enumState.itemsPtr[counter++]; {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_38_50v39g0n3ml22_nccmtc1m5r0000gp_T_main_d36a2c_mi_0, obj);
};
__continue_label_1: ;
} while (counter < limit);
} while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
((id)l_collection,
sel_registerName("countByEnumeratingWithState:objects:count:"),
&enumState, (id *)__rw_items, (_WIN_NSUInteger)16)));
obj = ((id)0);
__break_label_1: ;
}
else
obj = ((id)0);
}
return 0;
}
看著好惡心,一堆轉(zhuǎn)換。這里我們來簡(jiǎn)單翻譯一下(objc_msgsend 這種的妖,感興趣的自己查哈):
int main(int argc, const char * _Nullable argv[])
{
NSArray *array = @[@0, @1, @2];
{
id obj; /// 我們?cè)?for in 中聲明的 obj 本地變量
struct NSFastEnumerationState enumState = { 0 }; /// 初始化一個(gè)所有 fields 都為 0 的 NSFastEnumerationState 結(jié)構(gòu)體
id __rw_items[16]; /// 一個(gè) C 對(duì)象數(shù)組
id l_collection = (id) chris___array; /// 對(duì)原數(shù)組(chris___array)的一個(gè)重新聲明
NSUInteger limit = [l_collection countByEnumeratingWithState:&enumState objects:__rw_items count:16]; /// 向數(shù)組發(fā)送 -[id countByEnumeratingWithState:objects:count:] 消息绣檬,并將結(jié)果保存在本地變量 limit 中
if (limit) { /// 由于 Limit 是 unsigned 無符號(hào),所以這里可以直接用 if 判斷是否為 0嫂粟,不用考慮負(fù)數(shù)的存在(平時(shí)大家還是減少這種代碼娇未,有點(diǎn)令人疑惑,比如寫成 if (limit > 0) 會(huì)好很多)
unsigned long startMutations = *enumState.mutationsPtr; /// 將 enumState 的 mutationsPtr 指針保存下來星虹,如果后面這個(gè)指針被修改零抬,證明在迭代過程中有修改容器的操作(比如邊迭代邊向容器中插入元素)
do { /// 第一層循環(huán), 循環(huán)向數(shù)組發(fā)送 -[id countByEnumeratingWithState:objects:count:] 直到遍歷結(jié)束(返回個(gè)數(shù) 0),可能一次調(diào)用 -[id countByEnumeratingWithState:objects:count:] 返回不了全部的內(nèi)容宽涌,所以需要兩次迭代
unsigned long counter = 0; /// 聲明本地變量 counter平夜,并置為 0
do { /// 第二層循環(huán), 遍歷一次 limit
if (startMutations != *enumState.mutationsPtr) { /// 先判斷是否有在迭代過程中修改容器的情況卸亮,如果有拋出異常
objc_enumerationMutation(l_collection); /// 拋出異常
}
obj = (id)enumState.itemsPtr[counter++]; /// 從 enumState 的 itemsPtr 變量中忽妒,取出來第 counter 個(gè)元素
{
NSLog(@"%@", obj); /// 使用取到的元素(一次遍歷)
}
} while (counter < limit);
} while ( (limit = [l_collection countByEnumeratingWithState:&enumState objects:__rw_items count:16]) );
} else {
/// 如果遍歷結(jié)束(返回個(gè)數(shù) 0),將本地變量 obj 置為 nil
obj = nil;
}
}
return 0;
}
經(jīng)過我們粗糙的翻譯兼贸,這段代碼簡(jiǎn)化了 n 多段直,看起來也更加直接。for...in 展開后的代碼溶诞,現(xiàn)在看起來并沒有那么神秘了鸯檬。不斷的向容器發(fā)送 -[id countByEnumeratingWithState:objects:count:] 消息,直到遍歷結(jié)束(返回 0)很澄。每次取出來的內(nèi)容京闰,通過 enumState->itemsPtr 引用獲取。所以用到的參數(shù)/本地變量共有三個(gè):
- -[id countByEnumeratingWithState:objects:count:] 的返回值甩苛,作為外層循環(huán)的條件蹂楣;
- 通過 C 指針傳入的 enumState 結(jié)構(gòu)體,其中真正遍歷的時(shí)候用到的字段共 2 個(gè):
- mutationsPtr:每次迭代通過 == 判斷是否有在迭代過程中修改容器的情況讯蒲,如果有痊土,拋出異常;
- itemsPtr:每次迭代墨林,從 itemsPtr 依次讀取元素赁酝,直到讀取完(count == limit)。
看完這段代碼之后旭等,NSFastEnumeration 這個(gè)協(xié)議看起來不再神秘酌呆,我們只要在 -[id countByEnumeratingWithState:objects:count:] 將 state 參數(shù)的 itemsPtr 變量設(shè)置正確,并且在方法結(jié)束的時(shí)候搔耕,返回正確的 itemsPtr 引用數(shù)組的個(gè)數(shù)隙袁。注意:這里我們可以一次返回多個(gè)數(shù)據(jù),也可以返回一個(gè)數(shù)據(jù),返回?cái)?shù)據(jù)個(gè)數(shù)為 0 的時(shí)候菩收,遍歷結(jié)束梨睁。通常來說,我們應(yīng)該一次返回一個(gè)元素來給調(diào)用者遍歷娜饵,因?yàn)檎{(diào)用者有可能在某些情況打破( break )循環(huán)坡贺,如果一次處理掉所有的指針,是否有效率上的浪費(fèi)箱舞?這里就需要根據(jù)自己的場(chǎng)景來酌情處理遍坟。
實(shí)現(xiàn)一個(gè)支持 NSFastEnumeration 的容器類
先來看一下我們 demo 容器類 XXCustomSet 的聲明:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 自定義容器類,接受了 NSFastEnumeration 協(xié)議
@interface XXCustomSet<__covariant ObjectType> : NSObject<NSFastEnumeration>
/// 返回容器中的所有元素
@property (nonatomic, copy, readonly, nullable) NSArray<ObjectType> *allObjects;
#pragma mark - Public
/// 向容器中添加一個(gè)對(duì)象
- (void)addObject:(ObjectType)object;
/// 移除容器中的一個(gè)對(duì)象
- (void)removeObject:(ObjectType)object;
@end
NS_ASSUME_NONNULL_END
XXCustomSet 的實(shí)現(xiàn):
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 內(nèi)部實(shí)際存儲(chǔ)的可變數(shù)組容器
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
@end
@implementation XXCustomSet
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
return 0;
}
@end
NS_ASSUME_NONNULL_END
代碼非常簡(jiǎn)單褐缠,接下來我們來開始實(shí)現(xiàn) NSFastEnumeration 協(xié)議政鼠。
- 支持遍歷
為了支持讓外部可以用 for...in 來遍歷我們的容器,我們需要在 -[id countByEnumeratingWithState:objects:count:] 方法中設(shè)置正確的 state->itemsPtr队魏。代碼如下(注:只修改了 -[id countByEnumeratingWithState:objects:count:] 方法):
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 內(nèi)部實(shí)際存儲(chǔ)的數(shù)組
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
@end
@implementation XXCustomSet
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 思路:初始狀態(tài), state->state 為 0 万搔,每次讓 state->state 增加 1 胡桨,然后我們?cè)O(shè)置 state->itemsPtr 為 buffer 指針,并且設(shè)置 buffer 的第一個(gè)元素為當(dāng)前遍歷的元素
/// 簡(jiǎn)單來說瞬雹,以 state->state 為下標(biāo)昧谊,取出我們內(nèi)部數(shù)組中的元素,放進(jìn) buffer 的第一個(gè)元素中酗捌,并且設(shè)置 state->itemsPtr 指針為 buffer呢诬,這樣結(jié)合我們 rewrite 之后的代碼來看,結(jié)果就變成了每次讀取數(shù)組中當(dāng)前 state 下標(biāo)的元素胖缤,一直讀取到結(jié)束
if (state->state >= self.internalAllObjects.count) {
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
return 1;
}
@end
NS_ASSUME_NONNULL_END
看起來我們這個(gè)代碼應(yīng)該可以正常運(yùn)行了尚镰,那么我們來執(zhí)行一遍,測(cè)試代碼如下:
#import <Foundation/Foundation.h>
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
int main(int argc, const char * _Nullable argv[])
{
XXCustomSet *set = [XXCustomSet new];
for (NSUInteger index = 0; index < 30; index++) {
[set addObject:@(index)];
}
for (id number in set) { /// 執(zhí)行后哪廓,這一行掛了狗唉,報(bào)錯(cuò):Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)
NSLog(@"%@", number);
}
return 0;
}
NS_ASSUME_NONNULL_END
WTF? 發(fā)生什么事了?
再仔細(xì)看了一下 rewrite 后的 cpp 代碼涡真,應(yīng)該是這一句:
unsigned long startMutations = *enumState.mutationsPtr; /// 將 enumState 的 mutationsPtr 指針保存下來分俯,如果后面這個(gè)指針被修改,證明在迭代過程中有修改容
由于我們沒有設(shè)置 mutationsPtr 指針哆料,那么這一句話的后果就是在 0 處取值缸剪,所以給了我們一個(gè) bad access, address=0x0。我們來做一些簡(jiǎn)單的修改东亦,讓代碼可以 正常 執(zhí)行(未修改部分使用 ... 省略):
...
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 遍歷終點(diǎn)
if (state->state >= self.internalAllObjects.count) {
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
state->mutationsPtr = (unsigned long *)(__bridge void*)self; /// 新增一行代碼
return 1;
}
...
執(zhí)行杏节,一切正常。但是,我們的 mutationsPtr 設(shè)置的真的正確嗎拢锹?
測(cè)試一下谣妻,添加一行測(cè)試代碼:
int main(int argc, const char * _Nullable argv[])
{
XXCustomSet *set = [XXCustomSet new];
for (NSUInteger index = 0; index < 30; index++) {
[set addObject:@(index)];
}
for (id number in set) {
if ([number isEqualToNumber:@15]) {
[set removeObject:number]; /// 在 number 為 15 的時(shí)候,刪除一個(gè)元素
}
NSLog(@"%@", number);
}
return 0;
}
執(zhí)行代碼之后卒稳,沒有崩潰蹋半,因?yàn)槲覀冏隽吮Wo(hù),所以數(shù)組不會(huì)越界充坑,但是我們?cè)诒闅v完 15 之后减江,數(shù)組長(zhǎng)度變了,導(dǎo)致了后續(xù)的 state->state 下標(biāo)取出來的數(shù)據(jù)捻爷,都大了 1辈灼,所以 log 完 15 之后,直接 log 了 17也榄,把 16 吃掉了Q灿ā!甜紫!所以降宅, Mike Ash 文章中 state->mutationsPtr = (unsigned long *) self,并不能保證囚霸,在迭代中修改容器的操作會(huì)在編譯器 crash 出來(就像 NSMutableArray 那樣)腰根。
好吧,那么我們?cè)撊绾卧O(shè)置一個(gè)合適的值呢拓型?這里我的一個(gè)簡(jiǎn)單(low bee)的想法额嘿,我們維護(hù)一個(gè)變量,用來判斷是否有在迭代中修改容器的行為劣挫,改動(dòng)后代碼如下:
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 內(nèi)部實(shí)際存儲(chǔ)的數(shù)組
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
/// 新增:添加一個(gè)用于判斷是否在迭代中修改容器的指針册养,在這,存一個(gè)編輯版本號(hào)
@property (nonatomic, unsafe_unretained) unsigned long *editingVersion;
@end
@implementation XXCustomSet
#pragma mark - Deinit
/// /// 新增:dealloc 的時(shí)候揣云,手動(dòng)釋放一下我們申請(qǐng)的內(nèi)存
- (void)dealloc
{
free(_editingVersion);
}
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
/// 新增捕儒,申請(qǐng)一份內(nèi)存,用來存版本號(hào)
_editingVersion = malloc(sizeof(unsigned long));
*_editingVersion = 0;
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
/// 新增:每次修改的時(shí)候邓夕,讓版本號(hào)自增1
(*self.editingVersion) += 1;
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
/// 新增:每次修改的時(shí)候刘莹,讓版本號(hào)自增1
(*self.editingVersion) += 1;
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 遍歷終點(diǎn)
if (state->state >= self.internalAllObjects.count) {
/// 遍歷結(jié)束,將編輯版本置回 0
*(self.editingVersion) = 0;
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
/// 新增:將 mutationsPtr 設(shè)置為版本號(hào)
state->mutationsPtr = self.editingVersion;
return 1;
}
@end
NS_ASSUME_NONNULL_END
再次執(zhí)行我們的測(cè)試代碼焚刚,在迭代中修改容器点弯,結(jié)果
2021-01-08 19:57:21.198900+0800 algorithm[46120:1777121] 0
2021-01-08 19:57:21.199299+0800 algorithm[46120:1777121] 1
2021-01-08 19:57:21.199336+0800 algorithm[46120:1777121] 2
2021-01-08 19:57:21.199360+0800 algorithm[46120:1777121] 3
2021-01-08 19:57:21.199379+0800 algorithm[46120:1777121] 4
2021-01-08 19:57:21.199397+0800 algorithm[46120:1777121] 5
2021-01-08 19:57:21.199418+0800 algorithm[46120:1777121] 6
2021-01-08 19:57:21.199447+0800 algorithm[46120:1777121] 7
2021-01-08 19:57:21.199473+0800 algorithm[46120:1777121] 8
2021-01-08 19:57:21.199516+0800 algorithm[46120:1777121] 9
2021-01-08 19:57:21.199559+0800 algorithm[46120:1777121] 10
2021-01-08 19:57:21.199603+0800 algorithm[46120:1777121] 11
2021-01-08 19:57:21.199633+0800 algorithm[46120:1777121] 12
2021-01-08 19:57:21.199667+0800 algorithm[46120:1777121] 13
2021-01-08 19:57:21.199700+0800 algorithm[46120:1777121] 14
2021-01-08 19:57:21.199742+0800 algorithm[46120:1777121] 15
2021-01-08 19:57:21.203778+0800 algorithm[46120:1777121] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <XXCustomSet: 0x100475330> was mutated while being enumerated.'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff3039db57 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff692305bf objc_exception_throw + 48
2 CoreFoundation 0x00007fff3041630b __NSFastEnumerationMutationHandler + 159
3 algorithm 0x0000000100002f99 main + 457
4 libdyld.dylib 0x00007fff6a3d8cc9 start + 1
5 ??? 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <XXCustomSet: 0x100475330> was mutated while being enumerated.'
terminating with uncaught exception of type NSException
在遍歷完 15 之后,一個(gè) NSGenericException 異常拋了出來矿咕。
現(xiàn)在我們的 XXCustomSet 容器可以在 for...in 語句中抢肛,一次返回一個(gè)元素狼钮。當(dāng)然,我們也可以一次返回多個(gè)元素捡絮,比如 buffer 能容納的最大長(zhǎng)度 (僅列出改動(dòng)代碼熬芜,其余 ... 省略 ):
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 當(dāng)前使用 buffer 的長(zhǎng)度
NSUInteger usedBufferLen = 0;
/// 遍歷終點(diǎn)
if (state->state >= self.internalAllObjects.count) {
*(self.editingVersion) = 0;
return 0;
}
/// 這個(gè) for 有點(diǎn)奇怪,因?yàn)樗臈l件語句有點(diǎn)長(zhǎng)福稳,index < len 是判斷 index 不能超過 buffer 的長(zhǎng)度涎拉, state->state < self.internalAllObjects.count 是為了正確的讀取到容器的最后一個(gè)元素,不至于越界
for (NSUInteger index = 0; (index < len) && (state->state < self.internalAllObjects.count); index++) {
buffer[index] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
state->mutationsPtr = self.editingVersion;
/// 記錄當(dāng)前這一次的圆,使用到的 buffer 的長(zhǎng)度鼓拧,我們的例子中,第一次使用了 16 個(gè) buffer 長(zhǎng)度越妈,第二次使用了 14 個(gè)季俩,少兩個(gè)沒有用到,當(dāng)然我們不能在這里假設(shè) buffer 的長(zhǎng)度一定是 16 梅掠,還是要用 len 參數(shù)
usedBufferLen+= 1;
}
/// 返回當(dāng)前這一次數(shù)據(jù)的長(zhǎng)度
return usedBufferLen;
}
運(yùn)行代碼酌住, 一切都 ok 了。其實(shí)我們的測(cè)試代碼寫的有點(diǎn)刻意阎抒。我們內(nèi)部已經(jīng)持有了一個(gè)數(shù)組了赂韵,我們完全可以把消息直接發(fā)給數(shù)組:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
return [self.internalAllObjects countByEnumeratingWithState:state objects:buffer count:len];
}
一些思考
關(guān)于 mutationsPtr
這里其實(shí)可能不需要一個(gè)類似于版本號(hào)的代碼,比如我們用鏈表來實(shí)現(xiàn)一個(gè)棧挠蛉,我們內(nèi)部可以簡(jiǎn)單的將這個(gè)指針指向我們鏈表的棧頂(如果我們的棧是真正的只在棧頂做操作,并且相同的元素會(huì)有不同地址的節(jié)點(diǎn)來保存肄满,保證每個(gè)節(jié)點(diǎn)的內(nèi)存地址不同)谴古,每次修改棧的時(shí)候,因?yàn)闂m敹紩?huì)變化稠歉,所以就不需要版本號(hào)了掰担。版本號(hào)的代碼,還可以優(yōu)化一下怒炸,在遍歷的時(shí)候創(chuàng)建一個(gè)版本號(hào)带饱,在遍歷結(jié)束的時(shí)候釋放。為什么我們每次都將 state->itemsPtr 設(shè)置為 buffer阅羹,用別的指針不行么勺疼?
其實(shí)是可以的,代碼上都可以執(zhí)行捏鱼。比如我們內(nèi)部就是用這樣的指針來維護(hù)执庐,那么我們完全可以不用 buffer,也不需要管 len导梆,一次性就可以返回所有的元素轨淌;我們也可以自己申請(qǐng)一段內(nèi)存迂烁,然后注意釋放,代碼如下:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 初始狀態(tài)递鹉,我們創(chuàng)建一個(gè)能夠容納我們數(shù)組的地址盟步,并將指針存到 state->extra 中
if (state->state == 0) {
id __unsafe_unretained * itemsPtr = (id __unsafe_unretained *)malloc(sizeof(id) * self.internalAllObjects.count);
/// 將數(shù)組中的每一個(gè)元素,放進(jìn)我們創(chuàng)建的 itemsPtr 中
for (NSUInteger index = 0; index < self.internalAllObjects.count; index++) {
itemsPtr[index] = self.internalAllObjects[index];
}
/// 將 state->itemsPtr 指向我們創(chuàng)建的 itemsPtr
state->itemsPtr = itemsPtr;
/// 存一下我們創(chuàng)建的 itemsPtr 躏结,在結(jié)束的時(shí)候却盘,釋放內(nèi)存,防止泄露
/// 注:看代碼窜觉,我們好像也可以不用 extra 字段谷炸,在結(jié)束的時(shí)候直接釋放 state->itemsPtr ,但是如果系統(tǒng)修改了這個(gè)字段(雖然現(xiàn)在好像沒有)禀挫,就掛了旬陡,所以我們將創(chuàng)建的 itemsPtr 存入約定的、給我們自己隨意玩的 extra 字段中
state->extra[0] = (unsigned long)itemsPtr;
/// 設(shè)置 mutationsPtr 和之前一樣
state->mutationsPtr = self.editingVersion;
/// 修改 state->state 狀態(tài)语婴,再次進(jìn)入方法的時(shí)候描孟,走到遍歷結(jié)束的分支
state->state = 1; /// 隨意的值,1000 都可以
return self.internalAllObjects.count;
}
/// 一次遍歷就結(jié)束了砰左,所以匿醒,如果 state->state 不是 0 ,我們直接清理內(nèi)存缠导,然后返回 0
*(self.editingVersion) = 0;
id __unsafe_unretained * storedItemsPtr = (id __unsafe_unretained *)(void*)state->extra[0];
free(storedItemsPtr);
return 0;
}
- NSEnumerator 還記文章開頭廉羔,關(guān)于 NSFastEnumeration 中的注釋么?
The abstract class NSEnumerator provides a convenience implementation that uses nextObject to return items one at a time. 抽象的 NSEnumerator 類僻造,提供了使用 nextObject 在同一時(shí)間返回對(duì)象的便利實(shí)現(xiàn)憋他。
我們也可以實(shí)現(xiàn)我們?nèi)萜髯约旱?enumerator ,來一次返回一個(gè)對(duì)象髓削。這個(gè)大家自己嘗試吧竹挡。
一個(gè)可以運(yùn)行的 demo
我比較懶,就給出一個(gè)可執(zhí)行的 demo 吧立膛,也就是我們一次返回一個(gè)元素的版本:
首先是類的聲明:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet<__covariant ObjectType> : NSObject<NSFastEnumeration>
/// 返回容器中的所有元素
@property (nonatomic, copy, readonly, nullable) NSArray<ObjectType> *allObjects;
#pragma mark - Public
/// 向容器中添加一個(gè)對(duì)象
- (void)addObject:(ObjectType)object;
/// 移除容器中的一個(gè)對(duì)象
- (void)removeObject:(ObjectType)object;
@end
NS_ASSUME_NONNULL_END
接著是類的實(shí)現(xiàn):
#import "XXCustomSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface XXCustomSet ()
/// 內(nèi)部實(shí)際存儲(chǔ)的數(shù)組
@property (nonatomic, strong, readonly) NSMutableArray *internalAllObjects;
/// 添加一個(gè)用于判斷是否在迭代中修改容器的指針揪罕,在這,存一個(gè)編輯版本號(hào)
@property (nonatomic, unsafe_unretained) unsigned long *editingVersion;
@end
@implementation XXCustomSet
#pragma mark - Deinit
- (void)dealloc
{
free(_editingVersion);
}
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self) {
_internalAllObjects = [NSMutableArray array];
_editingVersion = malloc(sizeof(unsigned long));
*_editingVersion = 0;
}
return self;
}
#pragma mark - Public
- (void)addObject:(id)object
{
[self.internalAllObjects addObject:object];
(*self.editingVersion) += 1;
}
- (void)removeObject:(id)object
{
[self.internalAllObjects removeObject:object];
(*self.editingVersion) += 1;
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(__unsafe_unretained id _Nullable [])buffer
count:(NSUInteger)len
{
/// 遍歷終點(diǎn)
if (state->state >= self.internalAllObjects.count) {
*(self.editingVersion) = 0;
return 0;
}
buffer[0] = self.internalAllObjects[state->state];
state->itemsPtr = buffer;
state->state++;
state->mutationsPtr = self.editingVersion;
return 1;
}
@end
NS_ASSUME_NONNULL_END
寫在最后
如有錯(cuò)誤宝泵,或者好的想法好啰,大家可以在評(píng)論區(qū)發(fā)言,我盡可能積極配合鲁猩。
愿大家平安坎怪,健康。 Bye~