March 18, 2009

Copying pitfalls

Copy semantics are a little different in Objective-C than in Java and C++. Say we have an array:
NSArray* arr1 = [[NSArray alloc] init];
And you have some data in it. Most people realize that the following assignment
NSArray* arr2 = arr1;
doesn't make a copy of anything except the pointer. What you end up with is two pointers pointing to the same array. To make a shallow copy of arr1, you can do something like this:
NSArray* arr2 = [[NSArray alloc] initWithArray:arr1];
This is a "shallow" copy because although it creates a new array, all its contents are pointers which point to the same contents of arr1. The Objective-C frameworks provide a copy method which you would think would work, but it too creates a shallow copy. The following is just another way of saying the above:
NSArray* arr2 = [arr1 copy];
This will also allocate a new NSArray object (and retain it) and put in the same pointers from arr1 into arr2. To get a deep copy of arr1, meaning not only a new array but a new copy of each element in the array, you need something different:
NSArray* arr2 = [[NSArray alloc] initWithArray:arr1 copyItems:YES];
NSDictionary objects also provide this form of initializer to allow deep copying of a dictionary container. All of this is fairly muddy, but when we start talking about mutable containers, it gets worse. Consider the following code:
NSMutableDictionary* firstDict = [[NSMutableDictionary alloc] initWithCapacity:10];

// add some data to the dictionary
[firstDict setObject:@"hi" forKey:@"one"];

// now make a copy of it
NSMutableDictionary* secondDict = [firstDict copy];

// add some more data to the second dictionary
[secondDict setObject:@"there" forKey:@"two"];

// BOOM!

The "BOOM!" above means an exception is thrown. Why? Because the dictionary secondDict, although declared as an NSMutableDictionary and copied from another NSMutableDictionary is actually an NSDictionary. It is reasonable to think that copying a mutable into another mutable would create a mutable copy, but it is not so in Objective-C. The problem is that the copy method creates an immutable object. The solution here is to use the mutableCopy method, which is a special copy method just for mutable containers. So the line above should be replaced with:
NSMutableDictionary* secondDict = [firstDict mutableCopy];
So what if you want a deep copy of a mutable container? In this case, you can use the special NSDictionary initializer that takes the copyItems parameter:
NSMutableDictionary* secondDict = [[NSMutableDictionary alloc] initWithDictionary:firstDict copyItems:YES];
(Note that to use copy, mutableCopy, or the initializers that take copyItems, the objects that are contained in the array or dictionary must conform to the NSCopying and/or NSMutableCopying protocols.) What these examples show is that the initializers behave as expected, but the copy method is only for immutable containers and the mutableCopy method is for mutable containers. Because of this confusion, I prefer to use the initializer for both my shallow copying and deep copying; simply change the copyItems to NO for a shallow copy.

4 comments:

  1. Great write up. One question though:

    For your NSMutableDictionary issues, why not use addEntriesFromDictionary? I do a similar thing where I need to add the contents of one mutable array to another (for what is essentially a copy). To accomplish this I init the second mutable array then use addObjectsFromArray. It seems NSMutableDictionary has a corresponding function called addEntriesFromDictionary.

    ReplyDelete
  2. I'm not sure if you're expecting a shallow or a deep copy. According to the docs, both addEntriesFromDictionary and addEntriesFromDictionary shallow copy the values. In other words, all it does is copy the pointers with a retain.

    Here is the information quoted from the Apple reference docs:

    Each value object from otherDictionary is sent a retain message before being added to the receiver. In contrast, each key object is copied (using copyWithZone:—keys must conform to the NSCopying protocol), and the copy is added to the receiver.

    Similar text is for the NSArray. I don't know why the API has so many different ways to get a shallow copy.

    ReplyDelete
  3. JES, you are totally correct. I'm new to Objective-C and I guess still didn't fully understand the difference between a shallow and deep copy. I was expecting a deep copy - this has cleared that up.

    Thanks to you pointing that out, I fixed a bug in my program that's been annoying me for a few weeks now. I was using addObjectsFromArray and didn't realize I was getting a shallow copy when I was expecting a deep one. After adding copyWithZone and that crucial copyItems:YES everything is working as expected. I thank you for that!

    I'm with you - not so sure why there are so many ways to get a shallow copy.

    ReplyDelete
  4. gteat article.......i understand it now.

    ReplyDelete