Item 22: Understand the NSCopying Protocol
A common thing to want to do with an object is to copy it. In Objective-C, this is done through the use of the copy
method. The way in which you can support copying of your own classes is to implement the NSCopying
protocol, which contains a single method:
- (id)copyWithZone:(NSZone*)zone
This harks back to the day when NSZone
was used to segment memory into zones, and objects were created within a certain zone. Nowadays, every app has a single zone: the default zone. So even though this is the method you need to implement, you do not need to worry about what the zone parameter is.
The copy
method is implemented in NSObject
and simply calls copyWithZone:
with the default zone. It’s important to remember that even though it might be tempting to override copy
, it’s copyWithZone:
that needs to be implemented instead.
To support copying, then, all you need to do is state that you implement theNSCopying
protocol and implement the single method within it. For example, consider a class representing a person. In the interface definition, you would declare that you implement NSCopying:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
@end
Then, you would implement the single required method of the protocol:
- (id)copyWithZone:(NSZone*)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
return copy;
}
This example simply passes all work in initializing the copy to the designated initializer. Sometimes, you might need to perform further work on the copy, such as if other data structures within the class are not set up in the initializer: for example, if EOCPerson
contained an array that was manipulated using a couple of methods to befriend and unfriend another EOCPerson
. In that scenario, you’d also want to copy the array of friends. Following is a full example of how this would work:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
@end
@implementation EOCPerson {
NSMutableSet *_friends;
}
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName {
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
_friends = [NSMutableSet new];
}
return self;
}
- (void)addFriend:(EOCPerson*)person {
[_friends addObject:person];
}
- (void)removeFriend:(EOCPerson*)person {
[_friends removeObject:person];
}
- (id)copyWithZone:(NSZone*)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
copy->_friends = [_friends mutableCopy];
return copy;
}
@end
This time, the copying method has been changed to additionally set the _friends
instance variable of the copy to a copy of its own _friends
instance variable. Note that the -> syntax has been used here, since the _friends
instance variable is internal. A property could have been declared for it, but since it’s never used externally, there is no need to do so.
An interesting question is raised by this example: Why is a copy taken of the_friends
instance variable? You could have just as easily not taken a copy, and each object would then share the same mutable set. But that would mean that if a friend were added to the original object, it would also magically become a friend of the copy. That’s clearly not what you would want in this scenario. But if the set were immutable, you might have chosen to not take a copy, since the set cannot change, anyway. This would save having two identical sets lying around in memory.
You should generally use the designated initializer to initialize the copy, just as in this example. But you wouldn’t want to do this when the designated initializer has a side effect that you don’t want to happen to the copy, such as setting up complex internal data structures that you are immediately going to overwrite.
If you look back up to the copyWithZone:
method, you will see that the friends set is copied using the mutableCopy
method. This comes from another protocol, calledNSMutableCopying
. It is similar to NSCopying
but defines the following method instead:
- (id)mutableCopyWithZone:(NSZone*)zone
The mutableCopy
helper is just like the copy helper and calls the preceding method with the default zone. You should implement NSMutableCopying
if you have mutable and immutable variants of your class. When using this pattern, you should not override copyWithZone:
in your mutable class to return a mutable copy. Instead, if a mutable copy is required of either an immutable or a mutable instance, you should use mutableCopy.
Similarly, if an immutable copy is required, you should use copy
.
The following holds true in the case of NSArray
and NSMutableArray:
-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray
Note the subtlety with calling copy
on a mutable object and being given an instance of another class, the immutable variant. This is done so that it is easy to switch between mutable and immutable variants. Another way this could have been achieved is to have three methods: copy
, immutableCopy,
and mutableCopy,
wherecopy
always returns the same class, but the other two return specific variants. However, this would not be good if you don’t know whether the instance you have is immutable. You may decide to call copy
on something you have been returned as anNSArray
but in fact it’s an NSMutableArray
. In that case, you would assume that you have an immutable array returned, but instead it’s a mutable one.
You could introspect (see Item 14) to determine what type of instance you have, but that would add complexity everywhere a copy is taken. So you would end up always using immutableCopy
or mutableCopy
to be on the safe side, in which case, it’s back to just having just two methods, which is exactly the same as having only copy
andmutableCopy
. The benefit of calling it copy
instead of immutableCopy
is thatNSCopying
is designed not only for classes with mutable and immutable variants but also cases in which there is not the distinction, so immutableCopy
would be a badly named method.
Another decision to make with your copying method is whether to perform a deep or a shallow copy. A deep copy copies all the backing data as well. Copying by default for all the collection classes in Foundation is shallow, meaning that only the container is copied, not the data stored within the container. This is mainly because objects within the container might not be able to be copied; also, it’s usually not desirable to copy every object. The difference between a deep and a shallow copy is illustrated inFigure 3.2.
Usually, you will want your own classes to follow the same pattern as used in the system frameworks, with copyWithZone:
performing a shallow copy. But if required, a method can be added to perform a deep copy. In the case of NSSet,
this is provided through the following initializer method:
- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems
If copyItems
is set to YES
, the items in the array are sent the copy
message to create a copy to build up the new set to return.
In the example of the EOCPerson
class, the set containing the friends is copied incopyWithZone:,
but as discussed, this does not copy the items in the set themselves. But if such a deep copy were required, you could provide a method such as the following:
- (id)deepCopy {
EOCPerson *copy = [[[self class] alloc]
initWithFirstName:_firstName
andLastName:_lastName];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return copy;
}
No protocol defines deep copying, so it is left up to each class to define how such a copy is made. You simply need to decide whether you need to provide a deep copy method. Also, you should never assume that an object conforming to NSCopying
will be performing a deep copy. In the vast majority of cases, it will be a shallow copy. If you need a deep copy of any object, either find the relevant method, or assume that you have to create your own, unless the documentation states that its implementation of NSCopying
is providing a deep copy.
Things to Remember
Implement the NSCopying
protocol if your object will need to be copied.
If your object has mutable and immutable variants, implement both theNSCopying
and NSMutableCopying
protocols.
Decide whether a copy will be shallow or deep, and prefer shallow, where possible, for a normal copy.
Consider adding a deep-copy method if deep copies of your object will be useful.