improper NSInternalInconsistencyException on NSMutableArray: mutating method sent to immutable object

I have a NSMutableArray instance, named arr1, stored in a NSMutableDictionary, using [writeToFile:] to serialize the dictionary.

After then I deserialize the dictionary using [contentsOfFile:] method. Then fetch the array, named arr2, from this new dictionary.


Then, when [arr2 addObject:aString] is called, intresting things happen:

If arr2 has at least 1 elements, the above call succeed and the aString object is added to arr2 successfully.

If arr2 is empty, an NSInternalInconsistencyException raised, says: mutating method sent to immutable object.


What is the difference? In the above two cases, [arr2 class] returns a "__NSCFArray" class, and [[arr2 class] superClass] returns NSMutableArray !!!

Since arr2 is kind of NSMutableArray, what does this " mutating method sent to immutable object" mean ?



The following are the sample code and its output:


    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSLog(@"%@", path);
    path = [path stringByAppendingPathComponent:@"arrayTest.plist"];
  
    NSMutableArray *marr1 = [NSMutableArray array];
    [marr1 addObject:@"arr1_ele1"];
    NSArray *marr11 = [NSArray arrayWithObjects:@"arr11_ele1", nil];
  
    NSMutableArray *marr2 = [NSMutableArray array];
    NSArray *marr22 = [NSArray array];
  
  
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    [dict setObject:marr1 forKey:@"arr1"];
    [dict setObject:marr2 forKey:@"arr2"];
    [dict setObject:marr11 forKey:@"arr11"];
  
    [dict writeToFile:path atomically:YES];
  
    NSLog(@"marr1 class:%@, marr11 class:%@", [marr1 class], [marr11 class]);
    NSLog(@"marr2 class:%@, marr22 class:%@", [marr2 class], [marr22 class]);
  
    NSLog(@"************ UnSerialized form file ******************");
    NSDictionary *dict2 = [NSDictionary dictionaryWithContentsOfFile:path];
    marr1 = [dict2 objectForKey:@"arr1"];
    marr11 = [dict2 objectForKey:@"arr11"];
    marr2 = [dict2 objectForKey:@"arr2"];
    marr22 = [[marr2 mutableCopy] autorelease];
  
    NSLog(@"marr1 class: %@, kind of NSMutableArray:%zd,  member of NSMutableArray:%zd, super:%@", [marr1 class], [marr1 isKindOfClass:[NSMutableArray class]], [marr1 isMemberOfClass:[NSMutableArray class]], [[marr1 class] superclass]);
    NSLog(@"marr2 class: %@, kind of NSMutableArray:%zd,  member of NSMutableArray:%zd, super:%@", [marr2 class], [marr2 isKindOfClass:[NSMutableArray class]], [marr2 isMemberOfClass:[NSMutableArray class]], [[marr2 class] superclass]);
  
    try {
        NSLog(@"trying to add marr1...");
        [marr1 addObject:@"ele2"];
    } catch (NSException *e) {
        NSLog(@"%@", e);
    }
  
    NSLog(@"marr1 class: %@, marr1 content: %@", [marr1 class], marr1);
  
    try {
        NSLog(@"trying to add marr11...");
        [marr11 addObject:@"ele2"];
    } catch (NSException *e) {
        NSLog(@"%@", e);
    }
  
    NSLog(@"marr11 class: %@, marr11 content: %@", [marr11 class], marr11);
  
  
    try {
        NSLog(@"trying to add marr2...");
        [marr2 addObject:@"ele2"];
    } catch (NSException *e) {
        NSLog(@"%@", e);
    }
  
    NSLog(@"marr2 class: %@, marr2 content: %@", [marr2 class], marr2);
  
    try {
        NSLog(@"trying to add marr22...");
        [marr22 addObject:@"ele22"];
    } catch (NSException *e) {
        NSLog(@"%@", e);
    }
  
    NSLog(@"marr22 class: %@, marr22 content: %@", [marr22 class], marr22);



Output results:


marr1 class:__NSArrayM, marr11 class:__NSArrayI

marr2 class:__NSArrayM, marr22 class:__NSArray0

************ UnSerialized form file ******************

marr1 class: __NSCFArray, kind of NSMutableArray:1, member of NSMutableArray:0, super:NSMutableArray

marr2 class: __NSCFArray, kind of NSMutableArray:1, member of NSMutableArray:0, super:NSMutableArray

trying to add marr1...

marr1 class: __NSCFArray, marr1 content: (

"arr1_ele1",

ele2

)

trying to add marr11...

marr11 class: __NSCFArray, marr11 content: (

"arr11_ele1",

ele2

)

trying to add marr2...

-[__NSCFArray insertObject:atIndex:]: mutating method sent to immutable object

marr2 class: __NSCFArray, marr2 content: (

)

trying to add marr22...

marr22 class: __NSCFArray, marr22 content: (

ele22

)

You've got way too much sample code, so it's hard to pull out what's relevant. However, I believe your problem starts with you assuming that '[NSDictionary dictionaryWithContentsOfFile:]' returns a mutable dictionary. It doesn't, because you've sent the message to the immutable class. If you use '[NSMutableDictionary dictionaryWithContentsOfFile:]' instead, you will get a mutable dictionary, but it's collection elements will be immutable. This is documented in the NSDictionary class reference.


Similarly, if you create a mutable copy of an immutable dictionary, it's a shallow copy. The copy will be mutable, but its elements won't be copied or made mutable.


So, the array in your 'marr2' variable will be an immutable array after re-reading, even though you've used Obj-C language dynamism to get the NSArray object into a NSMutableArray variable. What you need to do is make your own deep mutable copy after reading the dictionary back in.


The other thing that's causing confusion is that there's no actually NSMutableArray concrete class, distinct from a NSArray concrete class. Of course, NSArray and NSMutableArray are class clusters, so there are no instances ever of those exact types. Array objects are always of a concrete subclass, __NSCFArray in this case. This subclass has both the mutable and immutable behavior, with a (private, internal) flag that set when the instance is created saying whether mutable methods are allowed. That means, if you start investigating class inheritance structure starting from __NSCFArray, you get strange-seeming results.

Thanks for your reply.


I understand that " it's collection elements will be immutable".

One more strange thing I want to know is , why marr1 adn marr2 got the different behavior. The difference is marr1 has one element, and marr2 is empty. But we can successfully add elements to marr1, though an exception raised when adding objects to marr2.

>> we can successfully add elements to marr1


Perhaps, but you're not allowed to try. Taking a step back from your specific problem, you should know that it's an acceptable pattern for an API to return a mutable object when the return type is immutable. For example:


- (NSArray*) mySpecialArray
{
     NSMutableArray* result = [NSMutableArray array];
     … compute result …
     return result; // OK to do this, instead of [result copy]
}


You might do this, for example, to avoid having to make an expensize copy of the result array. Then the caller can do:


NSArray* specialArray = [someObject mySpecialArray];


The caller can use 'specialArray' as a NSArray, but it's breaking the rules if it tries to find out if the array mutable:


if ([specialArray isKindOfClass: [NSMutableArray class]]) { // no, no!
     NSMutableArray* mutableArray = (id) specialArray;
     [mutableArray addObject: …];
}


Similarly, 'dictionaryWithContentsOfFile:' is allowed to put a NSMutableArray in its result, but you're not allowed to rely on it. In your example, it looks like your empty array was created (and archived into the file) with a special "empty array" concrete subclass (__NSArray0), which happens to unarchive as an immutable __NSCFArray, while a non-empty mutable array (__NSArrayM) happen to unarchive as a mutable __NSCFArray. It's an interesting implementation detail, but that's all.

improper NSInternalInconsistencyException on NSMutableArray: mutating method sent to immutable object
 
 
Q