Saturday, July 13, 2013

Apple, why are you toying with me via -subviews? (mutability vs immutability)

You want to get a view's subviews and enumerate them.

Maybe you also want to remove some of them from their superview while you do that, why not?

But you have a slight misgiving because what if that would constitute mutation during enumeration?

So let's look at the API for -subviews:

@property(nonatomic,readonly,copy) NSArray *subviews;

Hey, this looks pretty good! It's an NSArray, so we know it's immutable. Therefore, no mutation. Very nice! 

And Apple even tells us how we should act in a similar scenario in our own code (my highlighting):

Consider a scenario where all objects are capable of being mutated. In your application you invoke a method and are handed back a reference to an object representing a string. You use this string in your user interface to identify a particular piece of data. Now another subsystem in your application gets its own reference to that same string and decides to mutate it. Suddenly your label has changed out from under you. Things can become even more dire if, for instance, you get a reference to an array that you use to populate a table view. The user selects a row corresponding to an object in the array that has been removed by some code elsewhere in the program, and problems ensue. Immutability is a guarantee that an object won’t unexpectedly change in value while you’re using it.

Sounds great to me, Apple! So let's do this:

    for (UIView *subview in parentView.subviews) {
        if (subview.tag == 37) {
            [subview removeFromSuperview];

Totally safe! But oh no!!

*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__nsarraym: 0x63f9a0=""> was mutated while being enumerated.'

How could this be??? Let's find out:

UIView *parentView = [[UIView alloc] init];
NSLog(@"subviews: %p class: %@ count: %d", parentView, [parentView.subviews class], [parentView.subviews count]);

logged result (unimportant stuff edited): 
2013-07-13 23:54:19.168 subviews: 0x71a2ea0 class: __NSArrayI count: 0

OK, this looks fine. __NSArrayI means an immutable array and yep it has zero items. Then what's up? I'll take the next step to see:

UIView *parentView = [[UIView alloc] init];
NSLog(@"subviews: %p class: %@ count: %d", parentView, [parentView.subviews class], [parentView.subviews count]);
UIView *childView1 = [[UIView alloc] init];
UIView *childView2 = [[UIView alloc] init];
[parentView addSubview:childView1];

NSLog(@"subviews: %p class: %@ count: %d", parentView, [parentView.subviews class], [parentView.subviews count]);

[parentView addSubview:childView2];
NSLog(@"subviews: %p class: %@ count: %d", parentView, [parentView.subviews class], [parentView.subviews count]);

This code results in this log:
2013-07-14 00:37:16.646 subviews: 0x712a170 class: __NSArrayI count: 0
2013-07-14 00:37:16.648 subviews: 0x712a170 class: __NSArrayM count: 1
2013-07-14 00:37:16.648 subviews: 0x712a170 class: __NSArrayM count: 2

Wait, what!? My nice immutable __NSArrayI has transformed into a dangerous, mutable __NSArrayM! Who asked for THIS? (and check it out, the immutable array and the mutable ones are the SAME OBJECT!) Why would Apple do this to me? Let's go look at that page some more, maybe we missed something! Sure enough, we did:

Although in theory immutability guarantees that an object’s value is stable, in practice this guarantee isn’t always assured. A method may choose to hand out a mutable object under the return type of its immutable variant; later, it may decide to mutate the object, possibly violating assumptions and choices the recipient has made based on the earlier value.

So OK, Apple. You say something totally reasonable in that first paragraph, then you throw it out the window in this paragraph?? Why?? Just how much advantage is there for you to send out an NSMutableArray sometimes instead of always sending out a nice safe NSArray? Would it kill you to just throw a -copy on your internal mutable -subviews array before you let me have it?

Apple says some other things on that page:

When the mutability of objects is an issue, it’s best to adopt some defensive programming practices.

Oh yeah, you don't say?

Rely on the return type for indications of mutability.

Yeah, that sounds like a GREAT idea! I think I will do that (if I want to crash).

If you have any doubts about whether an object is, or should be, mutable, go with immutable.
Exactly what I think you should be doing too, so why aren't you, Apple?

If you have a mutable collection that is frequently changed and that you frequently hand out to clients (that is, you return it directly in a getter accessor method), you run the risk of mutating something that your clients might have a reference to. If this risk is probable, the instance variable should be immutable.

Fantastic idea, Apple! I wish I had thought of it! I wish you had implemented it that way!

If the value of the instance variable frequently changes but you rarely return it to clients in getter methods, you can make the instance variable mutable but return an immutable copy of it in your accessor method

Yeah, this sounds great too! How about you start doing that? (even though -subviews doesn't qualify as "rarely", I don't care). This page is just full of great ideas that someone at Apple had, and someone else at Apple ignored!

One sophisticated approach for handling mutable collections that are returned to clients is to maintain a flag that records whether the object is currently mutable or immutable. If there is a change, make the object mutable and apply the change. When handing out the collection, make the object immutable (if necessary) before returning it.
Another gem! This is perfectly reasonable!! Why aren't you doing that? But then things really get weird on that page:

  • You ask NSView for its subviews (with the subviews method) and it returns an object that is declared to be an NSArray but which could be an NSMutableArray internally. Then you pass that array to some other code that, through introspection, determines it to be mutable and changes it. By changing this array, the code is mutating internal data structures of the NSView class.
So don’t make an assumption about object mutability based on what introspection tells you about an object. Treat objects as mutable or not based on what you are handed at the API boundaries (that is, based on the return type). 
I'm not making an assumption based on introspection (calling -isKindOfClass on it). I AM relying on what the API says! But you are kind of screwing me over, and even worse, you keep changing your mind about if I should be expecting it. Look at the split personalities of those two paragraphs.

It is you, Apple, who is mutating the array (supposedly an NSArray according to the API) that you pass to me, your beloved client. I didn't check it via introspection, just like you said, I checked the API return type!

So, devs, be sure to make your own copy of -subviews all over your code and don't forget either! All because Apple didn't want to return [_subviews copy] for some reason.

Or is it just a bug I should file?

7/18/2013 Postscript: Call -copy on an NSMutableURLRequest and see what kind of thing you get back!


At July 13, 2013 at 9:59 PM , Blogger Ethical Paul said...

And people are really confused about it over on Stack Overflow:

At July 14, 2013 at 4:23 AM , Blogger Mark said...

I'm feeling bad for the poor sods at Apple whose job is to try to write and maintain documentation that ends up sooner or later being internally inconsistent.

Apple should just give up and open-source iOS and OS X. Somehow I don't think they will.

Thanks Paul for doing the great detective work to uncover this issue.


Post a Comment

Subscribe to Post Comments [Atom]

<< Home