NSFastEnumeration

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)該也看到過這篇):

Implementing Fast Enumeration

讀完了這篇文章之后音比,好像明白了很多贷币,也大概明白了 for...in 的實(shí)現(xiàn)苛萎。我們自己來操作一遍。

  1. 以 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;
}
  1. 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è):

  1. -[id countByEnumeratingWithState:objects:count:] 的返回值甩苛,作為外層循環(huán)的條件蹂楣;
  2. 通過 C 指針傳入的 enumState 結(jié)構(gòu)體,其中真正遍歷的時(shí)候用到的字段共 2 個(gè):
    1. mutationsPtr:每次迭代通過 == 判斷是否有在迭代過程中修改容器的情況讯蒲,如果有痊土,拋出異常;
    2. 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é)議政鼠。

  1. 支持遍歷

為了支持讓外部可以用 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];
}

一些思考

  1. 關(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í)候釋放。

  2. 為什么我們每次都將 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;
}
  1. 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~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末廓握,一起剝皮案震驚了整個(gè)濱河市搅窿,隨后出現(xiàn)的幾起案子嘁酿,更是在濱河造成了極大的恐慌,老刑警劉巖男应,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闹司,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡沐飘,警方通過查閱死者的電腦和手機(jī)游桩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耐朴,“玉大人借卧,你說我怎么就攤上這事∩盖停” “怎么了铐刘?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)影晓。 經(jīng)常有香客問我镰吵,道長(zhǎng),這世上最難降的妖魔是什么挂签? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任疤祭,我火速辦了婚禮,結(jié)果婚禮上饵婆,老公的妹妹穿的比我還像新娘勺馆。我一直安慰自己,他們只是感情好侨核,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布谓传。 她就那樣靜靜地躺著,像睡著了一般芹关。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上紧卒,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天侥衬,我揣著相機(jī)與錄音,去河邊找鬼跑芳。 笑死轴总,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的博个。 我是一名探鬼主播怀樟,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼盆佣!你這毒婦竟也來了往堡?” 一聲冷哼從身側(cè)響起械荷,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虑灰,沒想到半個(gè)月后吨瞎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡穆咐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年颤诀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片对湃。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡崖叫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拍柒,到底是詐尸還是另有隱情心傀,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布斤儿,位于F島的核電站剧包,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏往果。R本人自食惡果不足惜疆液,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陕贮。 院中可真熱鬧堕油,春花似錦、人聲如沸肮之。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽戈擒。三九已至眶明,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間筐高,已是汗流浹背搜囱。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柑土,地道東北人蜀肘。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像稽屏,于是被迫代替她去往敵國(guó)和親扮宠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 基礎(chǔ)知識(shí) 快速枚舉有兩個(gè)優(yōu)點(diǎn)狐榔。一是坛增,實(shí)現(xiàn)快速枚舉后获雕,你可以直接使用for/in語法遍歷你的對(duì)象。二是轿偎,如果將快速枚...
    張霸天閱讀 1,853評(píng)論 5 1
  • 前言:集合類在每門編程語言中都有著非常重要的地位典鸡,每一門語言對(duì)于集合類的實(shí)現(xiàn)和提供的API也大同小異。相比于Jav...
    馬修王閱讀 4,720評(píng)論 14 10
  • 問題 下面的代碼輸出是什么坏晦?會(huì)不會(huì)Crash萝玷?如果Crash解釋一下原因。 答案 控制臺(tái)的輸出給出了所有的答案: ...
    猹_閱讀 691評(píng)論 0 5
  • 今天看了個(gè)alibaba的開源庫(kù)coobjc昆婿,看到了代碼中使用了實(shí)現(xiàn)NSFastEnumeration協(xié)議的類進(jìn)行...
    淡燃閱讀 315評(píng)論 0 0
  • 1球碉、LLVM(低級(jí)虛擬機(jī))的 Clang 編譯器來編譯 OC 程序 Clang(前端)-- LLVM(后端)Cla...
    赫子豐閱讀 1,320評(píng)論 0 11