When does -copy return a mutable object?
Asked Answered
Q

4

9

I read in Cocoa and Objective C: Up and Running that -copy will always return an immutable object and -mutableCopy will always return a mutable object:

It’s important to know that calling -copy on a mutable object returns an immutable version. If you want to copy a mutable object and maintain mutability in the new version, you must call -mutableCopy on the original. This is useful, though, because if you want to “freeze” a mutable object, you can just call -copy on it.

So I have something like this:

NSMutableURLRequest *req = [[NSMutableURLRequest alloc] init];
NSLog( @"%@", [req className] );               // NSMutableURLRequest
NSLog( @"%@", [[req copy] className] );        // NSMutableURLRequest
NSLog( @"%@", [[req mutableCopy] className] ); // NSMutableURLRequest

According to this previous answer:

You cannot depend on the result of copy to be mutable! Copying an NSMutableArray may return an NSMutableArray, since that's the original class, but copying any arbitrary NSArray instance would not.

This seems to be somewhat isolated to NSURLRequest, since NSArray acts as intended:

NSArray *arr = [[NSMutableArray alloc] init];
NSLog( @"%@", [arr className] );                 // __NSArrayM
NSLog( @"%@", [[arr copy] className] );          // __NSAraryI
NSLog( @"%@", [[array mutableCopy] className] ); // __NSArrayM

So...

  1. When does -copy return an immutable object (as expected) and when does it return a mutable object?
  2. How do I achieve the intended effect of getting a "frozen" copy of a mutable object that refuses to be "frozen"?
Quarantine answered 27/8, 2011 at 2:56 Comment(2)
I think you got the answer in your post.Nashoma
@Oscar A is true and B is false doesn't exactly prove a rule to determine whether C is true.Quarantine
W
13

I think you've uncovered a great rift between documentation and reality.

The NSCopying protocol documentation claims:

The copy returned is immutable if the consideration “immutable vs. mutable” applies to the receiving object; otherwise the exact nature of the copy is determined by the class.

But this is clearly wrong in some cases, as you've shown in your examples (and I've sent feedback to them about this via that documentation page).

But(#2) in my opinion, it doesn't actually matter and you shouldn't care.

The point of -copy is that it will return an object you can use with the guarantee that it will behave independently of the original. This means if you have a mutable object, -copy it, and change the original object, the copy will not see the effect. (In some cases, I think this means that -copy can be optimized to do nothing, because if the object is immutable it can't be changed in the first place. I may be wrong about this. (I'm now wondering what the implications are for dictionary keys because of this, but that's a separate topic...))

As you've seen, in some cases the new object may actually be of a mutable class (even if the documentation tells us it won't). But as long as you don't rely on it being mutable (why would you?), it doesn't matter.

What should you do? Always treat the result of -copy as immutable, simple as that.

Winifred answered 27/8, 2011 at 4:0 Comment(3)
Agreed. This doesn’t address the defensive programming use case, however: what if you want to expose an immutable object in an accessor? In that situation, you do care what copy returns. It would be nice to make an immutable copy just once and return it over and over — but there’s no way to do that, so you have to make a copy every time the accessor is called, just in case the copy is mutable.Ezraezri
@PaulCantrell Sure. IMO that is overly defensive; if you return NSArray, for example, anyone who wants to mutate it would first have to cast it to NSMutableArray, and I'd consider that a programmer error (or an "undefined behavior" kind of situation). Note that Swift solves this problem because Array is a value type :)Winifred
@Winifred Well, the correct degree of defensive programming always depends on what you’re defending against! But yes, Swift’s structs are the first solution to the mutability-checking problem I’ve seen that actually works in practice.Ezraezri
S
3

1) When does -copy return an immutable object (as expected) and when does it return a mutable object?

you should always treat it as the immutable variant. the mutable interface of the returned type should not be used. apart from optimizations, the answer should not matter and should be considered an implementation detail unless documented.

the obvious case: for a number of reasons, objc class clusters and class designs can be complex. returning a mutable copy could simply be for convenience.

2) How do I achieve the intended effect of getting a "frozen" copy of a mutable object that refuses to be "frozen"?

using the copy constructor of the immutable class is a good way (similar to St3fan's answer). like copy, it's not a guarantee.

the only reason i can think of as to why you would want to enforce this behaviour is for performance or to enforce a restricted interface (unless it's academic). if you want performance or a restricted interface, then you can simply encapsulate an instance of the type which copies on creation and exposes only the immutable interface. then you implement copy via retain (if that's your intent).

alternatively, you can write your own subclass and implement your own variant of copy.

final resort: many of the cocoa mutable/immutable classes are purely interface - you could write your own subclass if you need to ensure a particular behaviour -- but that's quite unusual.

perhaps a better description of why this should be enforced would be good - the existing implementations work just fine for the vast majority of developers/uses.

Syverson answered 27/8, 2011 at 4:18 Comment(0)
M
1

Bear in mind that there is not one copy implementation -- each class implements its own. And, as we all know, the implementation of the Objective C runtime is a little "loosey goosey" in spots. So I think we can say that mostly copy returns an immutable version, but some exceptions exist.

(BTW, what does this do:

NSArray *arr = [[NSMutable array] init];

?)

Mackenzie answered 27/8, 2011 at 3:3 Comment(0)
E
-1

The best way to turn an object into an mutable one is to use the mutable 'constructor'. Like for example:

NSArray* array = ...;
NSMutableArray* mutableArray = [NSMutableArray arrayWithArray: array];

Copy is used to make a copy of an object. Not to change it's mutability.

Engedi answered 27/8, 2011 at 3:53 Comment(1)
Either that or send it a mutableCopy message, if it implements NSMutableCopying—both are equally good ways to get a mutable copy. That's not what the questioner is asking about, though.Anjanetteanjela

© 2022 - 2024 — McMap. All rights reserved.