Home > front end >  Why can't I mutate an NSObject allocated as mutable, referenced as immutable, then cast back to
Why can't I mutate an NSObject allocated as mutable, referenced as immutable, then cast back to

Time:01-05

I'm allocating an NSMutableAttributedString, then assigning it to the attributedString property of an SKLabelNode. That property is an (NSAttributedString *), but I figured that I could cast it to an (NSMutableAttributedString *) since it was allocated as such. And then access its mutableString property, update it, and not have to do another allocation every time I want to change the string.

But after the cast, the object is immutable and an exception is thrown when I try to mutate it.

Is it true that I can't mutate an NSObject that was allocated as mutable just because it was referenced as immutable?

CodePudding user response:

Is it true that I can't mutate an NSObject that was allocated as mutable just because it was referenced as immutable?

No, your general intuition here is correct. Ignoring for a second the concept of "mutable" and "immutable" in general, but focusing on the subclassing relationship between NS<SomeType> and NSMutable<SomeType>: typically, Apple framework objects which have mutable/immutable counterparts have the mutable variant as a subtype of the immutable variant. Assigning a mutable variable to an immutable variable does not change anything about the stored variable, same as the following does not:

@interface Foo: NSObject @end
@implementation Foo @end

@interface Bar: Foo @end
@implementation Bar @end

Foo *f = [[Bar alloc] init];
NSLog(@"%@", f); // => <Bar: 0x6000014b0040>

You can see something similar with NSMutableAttributedString (though it's a little more complicated because NSAttributedString and subtypes form a class cluster:

NSAttributedString *s = [[NSMutableAttributedString alloc] initWithString:@"Hello"];
NSLog(@"%@", [s class]); // => NSConcreteMutableAttributedString

However: the key difference between assigning to a local variable like with f and s above, and assigning to an SKLabelNode's attributedText property lies in the property's definition:

@property(nonatomic, copy, nullable) NSAttributedString *attributedText;

Specifically, SKLabelNode performs a copy on assignment to its attributedText property, and performing a copy on an NSMutableAttributedString produces an immutable variant:

NSAttributedString *s = [[[NSMutableAttributedString alloc] initWithString:@"Hello"] copy];
NSLog(@"%@", [s class]); // => NSConcreteAttributedString

So, when you assign to your SKLabelNode in this way, it doesn't store your original instance, but a copy of its own — and it happens to be that this copy is immutable.


Note that this is behavior is a confluence of two things:

  1. SKLabelNode chooses to -copy the assigned variable; if it -retained it instead (e.g. @property(nonatomic, strong, nullable)), this would work as you expected
  2. NSMutableAttributedString returns an NSAttributedString from its -copy method, but it doesn't have to. In fact, most types return instancetype from -copy, but NSMutableAttributedString chooses to return an NSAttributedString from its -copy method. (Well, that is the point of the class cluster: -copy → immutable, -mutableCopy → mutable)

So in general, this need not be the case, but you will see this behavior for mutable/immutable class clusters which are implemented using these rules.

For comparison, with the Foo example above:

@interface Foo: NSObject @end
@implementation Foo
- (instancetype)copyWithZone:(NSZone *)zone {
    // Expects to return a new Foo:
    return [[[self class] alloc] init];

    // OR:
    // Not all types allow copying:
    return self;
}
@end

@interface Bar: Foo @end
@implementation Bar @end

Foo *f = [[[Bar alloc] init] copy];
NSLog(@"%@", f); // => <Bar: 0x600001e7c1a0>
  •  Tags:  
  • Related