为什么我不能改变分配为可变的 NSObject,引用为不可变的,然后转换回可变的?

Why can't I mutate an NSObject allocated as mutable, referenced as immutable, then cast back to mutable?

我正在分配一个 NSMutableAttributedString,然后将其分配给一个 SKLabelNode 的 attributedString 属性。 属性 是一个 (NSAttributedString *),但我想我可以将它转换为一个 (NSMutableAttributedString *),因为它是这样分配的。然后访问它的 mutableString 属性,更新它,而不必每次我想更改字符串时都进行另一次分配。

但是在转换之后,对象是不可变的,当我尝试改变它时抛出异常。

我真的不能改变一个被分配为可变的 NSObject 只是因为它被引用为不可变的吗?

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

不,您的一般直觉是正确的。暂时忽略一般“可变”和“不可变”的概念,但关注 subclassing NS<SomeType> 和 [=16= 之间的关系]:通常,具有mutable/immutable对应的Apple框架对象具有可变变体作为子类型 的不可变变体。将 mutable 变量分配给 immutable 变量不会改变存储变量的任何内容,与以下内容相同:

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

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

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

你可以看到与 NSMutableAttributedString 类似的东西(虽然它有点复杂,因为 NSAttributedString 和子类型形成 class cluster:

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

然而: 分配给 local 变量之间的主要区别,如上面的 fs ,并分配给 SKLabelNodeattributedText 属性 在于 属性 的定义:

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

具体来说,SKLabelNode 在分配给它的 attributedText 属性 时执行 copy,并在 NSMutableAttributedString 上执行复制] 产生一个 不可变的 变体:

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

因此,当您以这种方式分配给您的 SKLabelNode 时,它不会存储您的原始实例,而是存储它自己的一个副本 — 碰巧这个副本是不可变的。


请注意,这是两件事的结合:

  1. SKLabelNode 选择-copy赋值的变量;如果它 -retain 改为编辑它(例如 @property(nonatomic, strong, nullable)),这将按您预期的那样工作
  2. NSMutableAttributedString return 是 -copy 方法中的 NSAttributedString,但 没有 。事实上,大多数类型 return instancetype 来自 -copy,但是 NSMutableAttributedString 选择 到 return 一个 NSAttributedString 从它的 -copy 方法。 (好吧,这就是 class 集群的重点:-copy → 不可变,-mutableCopy → 可变)

所以一般来说,情况不一定如此,但您会在使用这些规则实施的 mutable/immutable class 集群中看到这种行为。

为了与上面的 Foo 示例进行比较:

@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>