Fast Enumeration over NSMutableArray problems

161 views Asked by At

I am having some issues trying to wrap my head around why we can not mutate a collection during enumeration... Apparently, if you are doing any sort of fast enumeration, the system should throw an exception if you try to mutate. Below I have three examples where I am mutating during enumeration. One is a simple C-style loop, and the other two use some form of fast-enumeration. I am only getting enumeration exceptions thrown for case 2. Shouldn't I also be getting exceptions thrown for case 1? Why is case 1 valid? Also, people throughout stack overflow say my case 3 is bad practice, but why? It is simple and seems to work. Inconsistency in how the two different fast-enumeration loops are behaving and the general disgust with the C-style loop is screwing with my understanding here. Instead of vague generic rules of thumb, if someone can really break this down to a science this would really help. From a fundamental level I want to know why the exceptions are not consistent here and why case 3 works for me when it apparently "shouldn't" or is "bad practice."

//Case 1:
NSMutableArray *array = [NSMutableArray arrayWithObjects:@"phuj", @"whub", @"adgh", nil];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [array removeObjectAtIndex:idx];
}];

//Case 2:
NSMutableArray *array = [NSMutableArray arrayWithObjects:@"phuj", @"whub", @"adgh", nil];
for (NSString *string in array) {
    [array removeObject:string];
}

//Case 3:
NSMutableArray *array = [NSMutableArray arrayWithObjects:@"phuj", @"whub", @"adgh", nil];
for (int i = 0; i < [array count]; i++) {
    [array removeObjectAtIndex:i];
}
1

There are 1 answers

1
KudoCC On BEST ANSWER

We love simple and straightforward code.

I won't talk about case 2, because it throws an exception and you can't mutate the array in it.

I want to start from an example, suppose we want to remove the "whub" item from a mutable array and the content of the array is "phuj, whub, whub, adgh", it has two whub as you see.

Let's start to write the code using c style loop:

for (int i = 0; i < [mArray1 count]; i++) {
    NSString *str = mArray1[i] ;
    if ([str isEqualToString:@"whub"]) {
        [mArray1 removeObjectAtIndex:i] ;
    }
}

The code has a bug, after it removes the first "whub", all the indexes of item located after it will decrease by 1, so the index of second whub should be 1 and i is 1 at the moment. In the next loop, i is 2, so it skips the second whub. You can change the code to make it correct.

for (int i = 0; i < [mArray1 count]; i++) {
    NSString *str = mArray1[i] ;
    if ([str isEqualToString:@"whub"]) {
        [mArray1 removeObjectAtIndex:i] ;
        --i ;
    }
}

It works, but it's not straightforward, we modify the index i and it makes the code complex.

Let's try to write the code using enumerateObjectsUsingBlock method like case 1:

NSMutableArray *array = [NSMutableArray arrayWithObjects:@"phuj", @"whub", @"whub", @"adgh", nil] ;
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    if ([obj isEqualToString:@"whub"]) {
        [array removeObjectAtIndex:idx] ;
    }
}];

If you build and run it, you will find the result array still contains one "whub" and this time, we can't modify the index to fix it.

Conclude: You talk much about that you can mutate the array in the loop, the fact you can mutate the array doesn't mean you will get the correct answer. In fact, with c style loop you can do what ever you want, it won't crash, but we say it is bad because we have a better way to go. In practice, we'd like to use simple code to achieve the target, so others can understand easily, ourselves can also benefit from it.

I'd like to use this way, I think it is simple and straightforward.

NSMutableArray *arrayRemove = [NSMutableArray array] ;
for (NSString *str in mArray1) {
    if ([str isEqualToString:@"whub"]) {
        [arrayRemove addObject:str] ;
    }
}
[mArray1 removeObjectsInArray:arrayRemove] ;