您的位置:首页 > 移动开发 > Objective-C

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

2013-12-07 23:02 429 查看


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 the
NSCopying
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:


Click here to view code image

#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:

Click here to view code image

- (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:

Click here to view code image

#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, called
NSMutableCopying
. 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:


Click here to view code image

-[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,
where
copy
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 an
NSArray
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
and
mutableCopy
. The
benefit of calling it
copy
instead of
immutableCopy
is
that
NSCopying
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.



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 in
copyWithZone:,
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:

Click here to view code image

- (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 the
NSCopying
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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: