iOS 遍历可变集合并移除集合内指定类别元素

开发中我们会遇到这样一种需求,在遍历可变集合时,移除集合内指定类别的元素。比如,移除所有Null类别的元素等。一般而言思路有两种,一种是双集合模式,另一种是单集合模式。双集合模式也叫副本模式,具体思路为,区分原元素集合及移除元素集合,匹配移除。一般有两种实现方式,以数组为例:
第一种,遍历原元素数组,将移除元素加入移除元素数组,最后统一从原元素数组移除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 原元素集合
NSMutableArray *array = @[@"1", @"11", @"111", [NSNull null], @"1111"].mutableCopy;
// 移除元素集合
NSMutableArray *removedArray = [NSMutableArray array];

for (id element in array)
{
if ([element isKindOfClass:[NSString class]])
{
[removedArray addObject:element];
}
}

[array removeObjectsInArray:removedArray];

NSLog(@"%@", array);

第二种与第一种类似,简化了些操作,定义一个副本,遍历副本找到匹配的元素,从移除元素数组中移除对应的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 移除元素集合
NSMutableArray *array = @[@"1", @"11", @"111", [NSNull null], @"1111"].mutableCopy;
// 原元素集合(副本)
NSMutableArray *copyArray = [NSMutableArray arrayWithArray:array];

for (id element in copyArray)
{
if ([element isKindOfClass:[NSString class]])
{
[array removeObject:element];
}
}

NSLog(@"%@", array);

双集合模式容易实现,也比较简单。但是由于额外创建了个集合副本,若集合比较大,会占用一定的内存空间。若不想使用双集合,我们也可以采用第二种思路,单集合模式,遍历单一集合的同时将指定元素直接从集合内移除。具体而言有三种实现方法,第一种是用for实现,以数组为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableArray *array = @[@"1", @"11", @"111", [NSNull null], @"1111"].mutableCopy;

for (NSUInteger i = array.count; i > 0; i--)
{
id element = array[i-1];

if ([element isKindOfClass:[NSString class]])
{
[array removeObject:element];
}
}

NSLog(@"%@", array);

这里,我们使用for遍历数组,并通过逆序将指定类别的元素移除出数组。而不使用正序移除原因在于,数组是有序集合,for遍历过程中,随着元素的移除,所移除元素之后的其他元素对应的数组下标也会同步前移,移除目标与下标不一致,导致结果与预期相反。以下为使用正序移除:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableArray *array = @[@"1", @"11", @"111", [NSNull null], @"1111"].mutableCopy;

for (NSUInteger i = 0; i < array.count; i++)
{
id element = array[i];

if ([element isKindOfClass:[NSString class]])
{
[array removeObject:element];
}
}

NSLog(@"%@", array);

移除结果:

1
2
3
4
5
6
7
8
9
(
11,
"<null>"
)

原数组元素 -> @"1", @"11", @"111", [NSNull null], @"1111"
移除[0]后 -> @"11", @"111", [NSNull null], @"1111"
移除[1]后 -> @"11", [NSNull null], @"1111"
移除[2]后 -> @"11", [NSNull null]

这里,数组并未越界,估计当可变数组移除元素后,内部实现中的count也是同步改变,提前结束循环。而逆序移除中,元素移除后,下标改变的是遍历过的元素,没有遍历的元素下标并未改变,不会出现这种情况。因而,有序集合应该尽量通过逆序遍历来移除元素。使用for进行遍历虽然比较简单,但缺点在于比较耗时,数据量比较大时,可以选择用forin 快速遍历实现。
直接使用forin遍历数组的同时,移除元素,系统会抛出异常:

1
Collection was mutated while being enumerated.

因而我们应当使用NSEnumerator这个枚举器实现forin逆序遍历,移除指定类别元素:

1
NSEnumerator is an abstract class, instances of whose subclasses enumerate collections of other objects, such as arrays and dictionaries.

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableArray *array = @[@"1", @"11", @"111", [NSNull null], @"1111"].mutableCopy;

NSEnumerator *enumerator = [array reverseObjectEnumerator];

for (id element in enumerator)
{
if ([element isKindOfClass:[NSString class]])
{
[array removeObject:element];
}
}

NSLog(@"%@", array);

使用forin进行遍历效率比较高,但是在遍历的过程中,我们无法直接获取元素的下标也无法直接修改被遍历的集合。若想保持遍历的高效,同时能方便获取集合内的每个元素,我们可以选择使用第三种实现方式,使用enumerateObjectsWithOptions:usingBlock:等遍历方法实现,在Block回调内将元素移除出数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableArray *array = @[@"1", @"11", @"111", [NSNull null], @"1111"].mutableCopy;

__weak typeof(array) weakArray = array; // retain cycle

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

if ([obj isKindOfClass:[NSString class]])
{
[weakArray removeObject:obj];
}
}];

NSLog(@"%@", array);

除了数组以外,我们也可在其他常用集合上实现:
字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableDictionary *dict = @{@"1":@"A", @"2":[NSNull null], @"3":@"B"}.mutableCopy;

__weak typeof(dict) weakDict = dict; // retain cycle

[dict enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {

if ([obj isKindOfClass:[NSString class]])
{
[weakDict removeObjectForKey:key];
}
}];

NSLog(@"%@", dict);

Set:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSMutableSet *set = [NSMutableSet setWithObjects:@"1", [NSNull null], @"2", nil];

__weak typeof(set) weakSet = set; // retain cycle

[set enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {

if ([obj isKindOfClass:[NSString class]])
{
[weakSet removeObject:obj];
}
}];

NSLog(@"%@", set);

至此,似乎已经完成需求。然而,以上方法都只是对集合根元素进行类别判断移除操作,并未考虑集合根元素为集合的情况(如数组的元素为数组),此时我们也需对其子元素进行类别判断移除操作。具体实现上,这里采用单集合思路,若根元素为移除类别,则直接移除;若根元素为集合时,先通过递归对根元素集合进行判断移除操作后,获取新的根元素集合,若新的根元素集合不为空则替换,否则移除。同时,我们仅考虑数组、字典及Set等常用集合(其他集合可自行添加实现),通过集合分类方法的形式实现,具体如下:
可变数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 替换或移除遍历的元素(若obj为nil则移除)
- (void)replaceEnumerateObjectAtIndex:(NSUInteger)index withObject:(id)obj
{
if (!obj)
{
[self removeObject:obj];
return;
}

[self replaceObjectAtIndex:index withObject:obj];
}

// 移除数组及数组元素内所有特定类别的元素
- (void)removeObjectsFromClass:(Class)aClass
{
if (!self || [self isKindOfClass:[NSNull class]]) return;

if (self.count == 0) return;

__weak typeof(self) weakSelf = self;

[self enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

if (obj==nil || [obj isKindOfClass:aClass])
{
[weakSelf removeObject:obj];
return;
}

if ([obj isKindOfClass:[NSDictionary class]])
{
obj = [obj dictionaryByFilteringOutObjectsFromClass:aClass];

[weakSelf replaceEnumerateObjectAtIndex:idx withObject:obj];
return;
}

if ([obj isKindOfClass:[NSArray class]])
{
obj = [obj arrayByFilteringOutObjectsFromClass:aClass];

[weakSelf replaceEnumerateObjectAtIndex:idx withObject:obj];
return;
}

if ([obj isKindOfClass:[NSSet class]])
{
obj = [obj setByFilteringOutObjectsFromClass:aClass];

[weakSelf replaceEnumerateObjectAtIndex:idx withObject:obj];
return;
}
}];
}

数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取移除特定类别元素后的新数组
- (NSArray *)arrayByFilteringOutObjectsFromClass:(Class)aClass
{
if (!self || [self isKindOfClass:[NSNull class]]) return nil;

if (self.count == 0) return self;

NSMutableArray *filteredArray = [NSMutableArray arrayWithArray:self];

[filteredArray removeObjectsFromClass:aClass];

return [filteredArray copy];
}

可变字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 替换或移除遍历的Key对应的元素(若obj为nil则移除)
- (void)replaceEnumerateObject:(id)object forKey:(NSString *)key
{
if (!key) return;
if (![[self allKeys] containsObject:key]) return;

if (!object)
{
[self removeObjectForKey:key];
return;
}

[self setObject:object forKey:key];
}

// 移除遍历的Key对应的特定类别的元素(若obj为nil则移除,否则替换Key对应的元素)
- (void)removeEnumerateObject:(id)obj fromClass:(Class)aClass forKey:(NSString *)key
{
if (!obj || [obj isKindOfClass:aClass])
{
[self removeObjectForKey:key];
return;
}

if ([obj isKindOfClass:[NSDictionary class]])
{
obj = [obj dictionaryByFilteringOutObjectsFromClass:aClass];

[self replaceEnumerateObject:obj forKey:key];
return;
}

if ([obj isKindOfClass:[NSArray class]])
{
obj = [obj arrayByFilteringOutObjectsFromClass:aClass];

[self replaceEnumerateObject:obj forKey:key];
return;
}

if ([obj isKindOfClass:[NSSet class]])
{
obj = [obj setByFilteringOutObjectsFromClass:aClass];

[self replaceEnumerateObject:obj forKey:key];
return;
}
}

// 移除字典内Key对应的特定类别的元素
- (void)removeObjectsFromClass:(Class)aClass forKey:(NSString *)key
{
if (!key) return;
if (![[self allKeys] containsObject:key]) return;

if (self == nil || [self isKindOfClass:[NSNull class]]) return;

id obj = [self objectForKey:key];

[self removeEnumerateObject:obj fromClass:aClass forKey:key];
}

// 移除字典内所有Keys对应的特定类别的元素
- (void)removeObjectsFromClass:(Class)aClass
{
if (self == nil || [self isKindOfClass:[NSNull class]]) return;

__weak typeof(self) weakSelf = self;

[self enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {

[weakSelf removeEnumerateObject:obj fromClass:aClass forKey:key];
}];
}

字典:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取移除所有Keys对应的特定类别元素的新字典
- (NSDictionary *)dictionaryByFilteringOutObjectsFromClass:(Class)aClass
{
if (!self || [self isKindOfClass:[NSNull class]]) return nil;

if (![self allKeys].count) return self;

NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithDictionary:self];
[dictionary removeObjectsFromClass:aClass];

return [dictionary copy];
}

可变Set:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 替换或移除遍历的元素(若otherObj为nil则移除)
- (void)replaceEenumerateObject:(id)obj withObject:(id)otherObj
{
[self removeObject:obj];

if (otherObj)
{
[self addObject:otherObj];
}
}

// 移除Set内所有特定类别的元素
- (void)removeObjectsFromClass:(Class)aClass
{
if (!self || [self isKindOfClass:[NSNull class]]) return;

if (self.count == 0) return;

__weak typeof(self) weakSelf = self;

[self enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
if (obj == nil || [obj isKindOfClass:aClass])
{
[weakSelf removeObject:obj];
return;
}

id targetObj = nil;

if ([obj isKindOfClass:[NSDictionary class]])
{
targetObj = [obj dictionaryByFilteringOutObjectsFromClass:aClass];

[weakSelf replaceEenumerateObject:obj withObject:targetObj];
return;
}

if ([obj isKindOfClass:[NSArray class]])
{
targetObj = [obj arrayByFilteringOutObjectsFromClass:aClass];

[weakSelf replaceEenumerateObject:obj withObject:targetObj];
return;
}

if ([obj isKindOfClass:[NSSet class]])
{
targetObj = [obj setByFilteringOutObjectsFromClass:aClass];

[weakSelf replaceEenumerateObject:obj withObject:targetObj];
return;
}
}];
}

Set:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取移除特定类别元素后的Set
- (NSSet *)setByFilteringOutObjectsFromClass:(Class)aClass
{
if (!self || [self isKindOfClass:[NSNull class]]) return nil;

if (self.count == 0) return self;

NSMutableSet *set = [NSMutableSet setWithSet:self];
[set removeObjectsFromClass:aClass];

return [set copy];
}

至此,基本实现完成,我们可移除集合内所有指定类别的元素:

1
2
3
4
5
6
7
8
9
10
NSMutableArray *arrayM = [NSMutableArray array];
arrayM[0] = @"0";
arrayM[1] = @[@"11", [NSNull null], @"12"];
arrayM[2] = @{@"A":@"21", @"B":@22, @"C":[NSNull null]};
arrayM[3] = [NSSet setWithObjects:[NSNull null], @"31", @"32", nil];
arrayM[4] = [NSNull null];

[arrayM removeObjectsFromClass:[NSString class]];

NSLog(@"%@", arrayM);

附,示例代码:https://github.com/ColinHwang/Demo-of-Tutorial-in-Blog/tree/master/iOS-EnumerateCollectionAndRemoveObjects

参考资料:

[1]张云龙.iOS开发遍历集合(NSArray,NSDictionary、NSSet)方法总结[EB/OL].http://www.jianshu.com/p/d6ef96c862ca ,2016-2-26.
[2]Zhangzhan_zg.遍历可变数组的同时删除数组元素的几种解决方案[EB/OL].http://blog.csdn.net/zhangzhan_zg/article/details/38453305 ,2014-8-14.
[3]Apple Inc.NSEnumerator Reference Class[EB/OL].https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSEnumerator_Class/index.html ,2016-7-27.