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:
SKLabelNode
chooses to-copy
the assigned variable; if it-retain
ed it instead (e.g.@property(nonatomic, strong, nullable)
), this would work as you expectedNSMutableAttributedString
returns anNSAttributedString
from its-copy
method, but it doesn't have to. In fact, most types returninstancetype
from-copy
, butNSMutableAttributedString
chooses to return anNSAttributedString
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>