Effective Objective-C 2.0: Item 22: Understand the NSCopying Protocol

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 classallocWithZone:zone]
                       initWithFirstName:_firstName
                             andLastName:_lastName];
    copy->_friends = [_friends mutableCopy];
    return copy;
}

@end

This time, the copying method has been changed to additionally set the _friendsinstance 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: copyimmutableCopy, 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 andmutableCopyThe 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.

Image

Figure 3.2 Shallow versus deep copy. The contents of a shallow copy point to the same objects as the original. The contents of a deep copy point to copies of the original contents.

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 classalloc]
                       initWithFirstName:_firstName
                             andLastName:_lastName];
    copy->_friends = [[NSMutableSet allocinitWithSet:_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

Image Implement the NSCopying protocol if your object will need to be copied.

Image If your object has mutable and immutable variants, implement both theNSCopying and NSMutableCopying protocols.

Image Decide whether a copy will be shallow or deep, and prefer shallow, where possible, for a normal copy.

Image Consider adding a deep-copy method if deep copies of your object will be useful.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值