美文网首页
如何实现一个自己的for...in...操作呢

如何实现一个自己的for...in...操作呢

作者: fanglaoda | 来源:发表于2017-06-25 18:28 被阅读0次

需求:对于一个自定义类如何也可以想和NSArrayNSDictionary一样可以直接遍历?

本篇目录:

  1. 解析系统for...in...的实现原理;
  2. 自己实现一个for...in...的类;
  3. 简单解释一下objc_enumerationMutation是如何抛出异常的。

1. 解析系统for...in...的实现原理

我们来看看苹果在2.0推出来的Fast Enumeration。
引用苹果官方文档的一段总结

The enumeration is more efficient than using NSEnumerator directly
The syntax is concise.
The enumerator raises an exception if you modify the collection while enumerating.
You can perform multiple enumerations concurrently.

翻译过来就是

  • 它比之前的NSEnumerator更高效
  • 语法更简洁
  • 如果这个集合在遍历的过程中修改了,会抛出异常
  • 可以同时执行对个枚举

Fast Enumeration是一个协议

@protocol NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state 
                                  objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer 
                                    count:(NSUInteger)len;

@end

这个方法的作用是根据具体数据个数返回一定数量的数组供调用者使用的,为什么是一定数量的数组呢,比如说数据源是有5个数据[@"1",@"2",@"3",@"4",@"5"],若调用者想要2个一组,那么需要3组才能完成;前面提到的一组,其实就是C语言的数组,而这个方法就是用来确定返回一个怎样的数组,方法的返回值就是对应数组的长度。

这个协议方法传3个参数分别是:

  1. state,它是个结构体;
```
 typedef struct {
        unsigned long state;
        id __unsafe_unretained _Nullable * _Nullable itemsPtr;
        unsigned long * _Nullable mutationsPtr;
        unsigned long extra[5];
    } NSFastEnumerationState;
```
  * `state`这个参数在for...in...的方法内部是没有使用的,是留给调用者备用的,用来记录一些状态;
  * `itemsPtr`就是C数组的指针,它和方法的返回共同构成了C语言数组;
  * `mutationsPtr`这个字段是用来记录在遍历的过程中,被遍历的对象有没有被改变,从而可以抛出异常;
  * `extra`这个和`state`字段一样,在for...in...的方法内部是没有使用的也是没有使用的,留给调用者使用的。
  1. buffer它是一个缓冲区,其实是一个C数组,因为在内存中不是所有的对象都是内存连续的,针对那些内存不连续,方法提供一个内存区域,调用者把数组都放到这个缓冲区,他的长度由len决定;
  2. len上面已经提到就是定义buffer长度的。

为何我会这样解释这些属性呢,我们来看看for...in...的C++实现,就可以一一验证上面的说法了

先写一个demo

#import <Foundation/Foundation.h>

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        NSMutableArray *arry = [NSMutableArray arrayWithObjects:@"1",@"2",@"3", nil];
        for (NSString *str  in arry) {
            NSLog(@"dddd---%@",str);
        }
        return 0;
        
}

使用命令

clang -rewrite-objc main.m

可以得到


int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSMutableArray *arry = ((NSMutableArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("arrayWithObjects:"), (id)(NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_0, (NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_1, (NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_2, __null);
        {
    NSString * str;
    
    // 这个就是传出去的State
    struct __objcFastEnumerationState enumState = { 0 };
    
    //这个就是buffer,可以看出是初始化了一个16长度的数组
    id __rw_items[16];
    
    //这个表示调用哪个对象的"countByEnumeratingWithState:objects:count:"方法
    id l_collection = (id) arry;
    
    //limit就是countByEnumeratingWithState:objects:count:返回值
    _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) {
    
    // 这里面有两个do...while...循环
    // 外层循环用来获取一共需要的数组的个数,并获取对应数组的长度
    // 内部循环是遍历获取对应数组的元素进行下一步操作
    
            //startMutations就是用来监控遍历的过程中遍历对象有没有改变
           unsigned long startMutations = *enumState.mutationsPtr;
            do {
                      unsigned long counter = 0;
                do {
                
                //如果遍历对象发生了改变就会调用`objc_enumerationMutation`来抛出异常
                    if (startMutations != *enumState.mutationsPtr)
                        objc_enumerationMutation(l_collection);
                        
                    // 取出对应的元素,这是高效快速的关键
                    str = (NSString *)enumState.itemsPtr[counter++]; 
                    {
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_g3_yzsthm0x1xs8k_ycscbf93hr0000gn_T_main_6d9606_mi_3,str);
                };
                // 结束这次循环,进行下一次
            __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)));
            str = ((NSString *)0);
            __break_label_1: ;
    }else
        str = ((NSString *)0);
    }

        return 0;


    }
}


对于上面的源码进行了一些必要的注释帮助大家理解,整个方法下来,并没有看到stateextra字段,这也验证了之前的说法。

2. 自己实现一个for...in...的类

这里参照苹果的官方demo写了一个简单的例子
对于countByEnumeratingWithState:objects:count:我们有两种方法来实现

  1. 对于在内存中连续的结合来说可以直接返回这段内存的首地址;
  2. 对于不连续的来说,这个时候就要使用buffer了,接下来分别给出两种方式

.h

#import <Foundation/Foundation.h>

@interface MyFastIterator : NSObject<NSFastEnumeration>


@end

.m

#import "MyFastIterator.h"
#include <vector>
#import <objc/message.h>

@interface MyFastIterator ()

@property (nonatomic, strong) NSArray *myArray;

@property (nonatomic, assign) long tagSi;

@end

@implementation MyFastIterator
{
    std::vector<NSNumber *> _list;
}

- (instancetype)init {
    if (self = [super init]) {
        for (NSUInteger i = 0; i < 17; i++) {
            _list.push_back(@(i));
        }
        
        self.myArray = @[@"1",@"2",@"3",@"4",@"5",@"6",
                         @"1",@"2",@"3",@"4",@"5",@"6",
                         @"1",@"2",@"3",@"4",@"5",
                         ];
        
    
        
    }
    return self;
}


countByEnumeratingWithState:objects:count:实现


#define USE_BUFF 1

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id  _Nullable __unsafe_unretained [])buffer count:(NSUInteger)len {
 NSUInteger count = 0;
    
    unsigned long countOfItemsAlreadyEnumerated = state->state;
    
    if (countOfItemsAlreadyEnumerated == 0 ) {// 等于0说明是第一次调用可以初始化一些数据
        NSLog(@"countByEnumeratingWithState");
        // 上面已经说到,mutationsPtr是用来记录遍历的过程中被遍历的对象有没有被修改的
        // 由于我们这里是NSArray是不可变的,所以无需追踪他的改变
        // 从而这里取 的是 &state->extra[0];
        state->mutationsPtr = &state->extra[0];
    }
#if USE_BUFF
    if (countOfItemsAlreadyEnumerated < self.myArray.count) {
        state->itemsPtr = buffer;
        
        while (countOfItemsAlreadyEnumerated < self.myArray.count && count < len){
//            NSLog(@"--%d",count);
            buffer[count] = self.myArray[countOfItemsAlreadyEnumerated];
            countOfItemsAlreadyEnumerated++;
            count++;
        }
    }else {
        count = 0;
    }
#else
    
    if (countOfItemsAlreadyEnumerated < _list.size()) {
        
        // 直接将 state->itemsPtr 指向内部的 C 数组指针,因为它的内存地址是连续的
        __unsafe_unretained const id * const_array = _list.data();
        
        state->itemsPtr = (__typeof__(state->itemsPtr))const_array;
        
        // 因为我们一次性返回了 _list 中的所有元素
        // 所以,countOfItemsAlreadyEnumerated 和 count 的值均为 _list 中的元素个数
        
        //  这里使用的是官方demo的写法
        countOfItemsAlreadyEnumerated = _list.size();
        count = _list.size();
    }else {
        count = 0;
    }
    
#endif
    
    state->state = countOfItemsAlreadyEnumerated;
    return count;

}

外面调用

- (void)testMyFastIterator {
    MyFastIterator *fast = [[ MyFastIterator alloc] init];
    for (NSNumber *num in fast) {
        NSLog(@"testMyFastIterator---%@",num);
    }
}

其实还有一种简单的写法,直接返回要遍历的对象的方法,前提是遍历的对象实现了countByEnumeratingWithState方法

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id  _Nullable __unsafe_unretained [])buffer count:(NSUInteger)len {
    
   return [self.myArray countByEnumeratingWithState:state objects:buffer count:len];
}

3. 简单解释一下objc_enumerationMutation是如何抛出异常的。

objc_enumerationMutation方法是如何抛出异常的呢,打开objc4-646的源码中可以看到具体实现



static void (*enumerationMutationHandler)(id);


void objc_enumerationMutation(id object) {
    if (enumerationMutationHandler == nil) {
        _objc_fatal("mutation detected during 'for(... in ...)'  enumeration of object %p.", (void*)object);
    }
    (*enumerationMutationHandler)(object);
}


void objc_setEnumerationMutationHandler(void (*handler)(id)) {
    enumerationMutationHandler = handler;
}

阅读源码可以得出看出:

  1. objc_setEnumerationMutationHandler方法接收一个函数指针,保存在内部定义的之前声明好的函数static void (*enumerationMutationHandler)(id);
  2. objc_enumerationMutation被调用的时候,如果调用者没有实现objc_setEnumerationMutationHandler的话,此时函数指针enumerationMutationHandler为nil,就会执行_objc_fatal("mutation detected during 'for(... in ...)' enumeration of object %p.", (void*)object);,否则就会通过*enumerationMutationHandler拿到函数并把object传递出去。

我们来看一个demo

//先初始化一个函数
void voidVoidTest(id objt) {
    NSLog(@"%@挂啦",objt);
}

- (void)testMutation {
    void (*funcVoidVoid)() = &voidVoidTest;
    objc_setEnumerationMutationHandler(funcVoidVoid);
    NSString *str = @"test";
    objc_enumerationMutation(str);
   } 
   

输出

test挂啦

相关文章

  • 如何实现一个自己的for...in...操作呢

    需求:对于一个自定义类如何也可以想和NSArray和NSDictionary一样可以直接遍历? 本篇目录: 解析系...

  • was mutated while being enumera

    was mutated while being enumerated, 如何解决For...in... ??中出现...

  • 单向链表的reverse实现(三)

    上一篇的MyLinkedList实现了一个双向链表,如何对单向链表实现reverse操作呢?下面是实现代码,基本思...

  • 循环操作

    循环操作 一、for循环 1、循环数组元素 2、循环对象属性:for...in... Array也是对象,而它的每...

  • 循环

    循环: for...in... while() (1)for...in... :依次把list或tuple中的每...

  • SQL语句对结果集操作

    前言 集合常见的操作是 交并差的操作,那么SQL语句是如何实现的呢? 1.交集 1.1 关键字 :InterSec...

  • IIS6配置301跳转到https的教程步骤

    申请SSL证书安装之后是需要进行301跳转操作,将所有http的链接跳转到https的。如何实现这个操作呢?接下来...

  • 堆和堆排序

    什么是堆? 如何存储一个堆(如何实现一个堆?) 堆的插入、删除操作 如何基于堆实现排序?(建堆和排序) 为什么快速...

  • RxSwift 项目实战举例

    本文非基础文章,主要举例实际开发中可能会遇到的场景。 如何实现多按钮互斥单选操作 如何实现数据的过滤操作 如何合并...

  • CAS 与、AQS(AbstractQueuedSynchron

    什么是原子操作?如何实现原子操作? 个人理解一个任务执行过程中不能打断必须按顺序执行且不可切割。 实现原子操作Ja...

网友评论

      本文标题:如何实现一个自己的for...in...操作呢

      本文链接:https://www.haomeiwen.com/subject/exikcxtx.html