Aug 24

oc数据集合(Collections)的三大语法糖与实现小记

Lrdcq , 2017/08/24 16:18 , 程序 , 閱讀(3715) , Via 本站原創
oc里面和集合类型数据最常用的语法,是数组的[1],字典的[@"key"]和大部分字典和集合类型都可以用的forin循环。这种语法在实战中确实很方便,那么我们自己编写的类型也可以使用这样的语法来方便实际开发过程。查询官方文档后,明显前两个的Subscript语法和forin语法可以分为这两类。

Subscript语法

数组和字典的属于Subscript语法,即常见的:
//
id a = list[1];
list[1] = a;
id b = map[@"key"];
map[@"key"] = b;
//

虽然没有提供接口(protocol),不过文档表现出来的就是固定的方法就可以了。其中Array对应的两个方法是:
//
- (id)objectAtIndexedSubscript:(NSUInteger)idx;
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx;
//

实测后发现,只要对应类型实现了这两个方法,xcode和编译器就不会报错,就可以正常使用。还有需要注意的是:

1. 因为没有对应的protocol,因此这两个方法的声明当然必须在头文件中暴露出来。

2. 由于oc的类型和方法无关也不存在重载,所以涉及到的id可以随意替换为具体类型,当然也可以是泛型。

3. 当然,还是由于oc的方法参数和类型无关,所以类型上可以随意修饰nullable之类的东西和其它protocol,完全不怂。

然后字典对应的Subscript方法也能轻而易举的找出来了:
//
- (id)objectForKeyedSubscript:(id)key;
- (void)setObject:(id)obj forKeyedSubscript:(id)key;
//

ForIn枚举

至于实现forin,经查询,我们需要实现一个枚举的protocol,即NSFastEnumeration(https://developer.apple.com/documentation/foundation/nsfastenumeration),只要实现了这个枚举接口的类型,用forin接口就不会报错了。

然而看NSFastEnumeration的那一个方法的具体内容,就是一脸懵逼了:
//protocol
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
//
//NSFastEnumerationState
typedef struct {
    unsigned long state;
    id __unsafe_unretained _Nullable * _Nullable itemsPtr;
    unsigned long * _Nullable mutationsPtr;
    unsigned long extra[5];
} NSFastEnumerationState;
//

仔细阅读文档后,原来这个方法希望我们返回一个id的c数组,即state->itemsPtr这一个指针,当然c数组还需要一个长度,就是这个方法的返回值。forin用while循环的取数组并挨个返回执行,直到周后一次调用这个方法后返回了空数组(返回值为0)就结束。

非要举一个栗子的话,比如我有一个10个长度的数组,
第一次调用,我返回了list和return 10,forin拿去遍历了,
第二次调用,我返回了return 0,forin结束。

或者我不一次返回回来,
第1-10次调用,我返回了list+i这个指针和1,forin就调了10次每次遍历长度为1的数组,
第11次调用,我返回return 0,forin结束。

这两种forin的最终效果是一致的,当然性能另说。

因此其他的参数都是冗余的辅助参数,其中state->mutationsPtr是有意义的,一旦这个指针指向的内容发生了改变,forin就会直接抛出oc异常,NSMutableArray在forin循环中改变即崩就是通过这个实现的。state->state和state->extra都是为了方便的让我记录循环的状态之类的东西的。

最后还有一个问题是,有些集合的实现里面,确实没有啥id的c数组,如果为了实现这个迭代接口,难道我还要特意准备一个c数组或者每次调用的时候生成一个c数组?这也太不方便了。考虑到这一点,这个接口第二参数就很好心的多提供了一个数组,你可以随意使用它把它递给state->itemsPtr,这样就好用多了吧。当然第三个参数就是第二个参数这个数组的长度了。

因此,看似复杂的借口,其实是非常简单的原理加上复杂的辅助参数组合起来的东西。

栗子

没看懂,举个栗子:这个是一个长度在初始化的时候就固定的可变数组的实现:
@interface ArrayLikeObject : NSObject <NSFastEnumeration>
- (instancetype)initWithNSArray:(NSArray *)data;
- (id)objectAtIndexedSubscript:(NSUInteger)idx;
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx;
@end

@implementation ArrayLikeObject {
    id __strong* _list;
    int _count;
}

- (instancetype)initWithNSArray:(NSArray *)data {
    if (self = [super init]) {
        _count = [data count];
        _list = (id __strong*)calloc(sizeof(id), _count);
        for (int i = _count - 1; i >= 0; i--) {
            _list[i] = data[i];
        }
    }
    return self;
}

- (void)dealloc {
    for (int i = 0; i < _count; i++) {
        _list[i] = nil;
    }
    free(_list);
}

- (id)objectAtIndexedSubscript:(NSUInteger)idx {
    if (idx >= _count) {
        return nil;
    }
    return _list[idx];
}

- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx {
    if (idx >= _count) {
        return;
    }
    _list[idx] = obj;
}

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
    if (state->state == 0) {
        state->mutationsPtr = &state->extra[0];
    }
    if (state->state < _count) {
        state->itemsPtr = (id __unsafe_unretained _Nullable * _Nullable)(id __unsafe_unretained const *)self->_list;
      state->state = _count;
        return _count;
    }
    return 0;
}
@end

//usage
  ArrayLikeObject *arr = [[ArrayLikeObject alloc] initWithNSArray:@[@"a1",@"a2",@"a3",@"a4",@"a5"]];
  
    NSLog(@"%@", arr[2]); //print a3
    arr[1] = @"ax";
    
    for (id x in arr) {
        NSLog(@"%@",x);
    }
    //print a1 ax a3 a4 a5
//

其中有几段需要解释一下:

1. 存储使用了id的c数组,在arc下这种数据是需要声明__strong或者__unsafe_unretained之类的,参看(http://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-ownership-restrictions)。另外在这段代码中,由于c数组就是核心的储存数组了,所以当然需要__strong。

2. c数组在__strong的情况下最好用calloc声明空间,如果用malloc的话,有一些坑,参看(https://stackoverflow.com/questions/13728487/occasional-exc-bad-access-when-assigning-value-in-c-id-strong-array)。另外对称的,既然是手动alloc出来的,也需要在dealloc手动置为nil(触发arc)和free掉c数组。

3. 迭代器的方法实现是用的上面栗子的第一个,也就是一口气返回完成。其中state->state初始值肯定是0,可以用它来判断是否是第一次运行。另外state->state一一般用于储存当前迭代的位置,以判断结束位。

4. state->mutationsPtr这里是初始化时随便指定了一个位置,并没有真正的使用。

5. state->itemsPtr的类型是__unsafe_unretained的,而我们储存用的__strong的,所以这里涉及到一个强转。虽然显然id的c数组而言这个修饰符并没有什么影响,不过直接强转编译器会报错,因此这里通过__unsafe_unretained const类型变换了一次,可能算是一个小技巧吧。不过这个东西强转的方式应该很多,毕竟强转只是骗过编译器类型检查而已。
关键词:ios , objective-c , 语法糖
logo