什么情况下使用-copy返回可变对象?

9

我在Cocoa and Objective C: Up and Running中读到,-copy总是返回一个不可变对象,而-mutableCopy总是返回一个可变对象:

重要的是要知道,在可变对象上调用-copy将返回一个不可变版本。如果你想复制一个可变对象并保持新版本的可变性,你必须在原始对象上调用-mutableCopy。这很有用,因为如果你想“冻结”一个可变对象,你只需要调用-copy

所以我有这样一段代码:

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

根据上一个答案:
引用:

你不能依赖复制的结果是可变的!复制NSMutableArray可能会返回NSMutableArray,因为那是原始类,但复制任何任意的NSArray实例都不会。

这似乎在NSURLRequest中有些孤立,因为NSArray的行为符合预期:

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

所以...

  1. -copy 何时返回一个不可变对象(如预期)以及何时返回一个可变对象?
  2. 我如何实现获取可变对象的“冻结”副本并拒绝被“冻结”的预期效果?

我认为你在帖子中得到了答案。 - Oscar Gomez
@Oscar _A为真_和_B为假_并不能确切地证明一个规则来确定C是否为真。 - rubergly
4个回答

13

我认为你揭示了文档与现实之间的巨大差距。

NSCopying协议文档声称:

如果“不可变 vs. 可变”适用于接收对象,那么返回的副本是不可变的;否则,副本的确切性质由类确定。

但是,在某些情况下,这显然是错误的,正如你在示例中所示(我已通过该文档页面向他们发送反馈)。

(#2) 在我看来,事实上并不重要,你不应该关心它。

-copy 的重点在于,它将返回一个对象,您可以使用该对象,并且保证其行为将独立于原始对象。这意味着,如果您有一个可变对象,将其进行-copy,并更改原始对象,则复制品将不会看到该效果。(在某些情况下,我认为这意味着-copy可以优化为什么都不做,因为如果对象是不可变的,它首先就无法更改。我可能对此有所错误。(我现在想知道这对字典键有什么影响,但那是另一个话题...))

正如你所看到的,在某些情况下,新对象实际上可能是可变类(即使文档告诉我们它不会)。但只要您不依赖它是可变的(为什么要这样做?),那么这并不重要。

你应该怎么做?始终将-copy的结果视为不可变的,就这么简单。


同意。然而,这并没有解决防御式编程的用例:如果您想在访问器中公开一个不可变对象怎么办?在这种情况下,您确实关心copy返回什么。最好只制作一个不可变副本并一遍又一遍地返回它 - 但是没有办法做到这一点,因此每次调用访问器时都必须进行复制,以防副本是可变的。 - Paul Cantrell
@PaulCantrell 当然可以。在我看来,这样做过于保守;例如,如果你返回NSArray,那么任何想要对其进行变异的人都必须首先将其转换为NSMutableArray,而我认为这是程序员错误(或者一种“未定义行为”类型的情况)。请注意,Swift解决了这个问题,因为Array是值类型 :) - jtbandes
@jtbandes 嗯,正确的防御式编程程度总是取决于你要防御什么!但是,是的,Swift的结构体是我见过的第一个实际可行的解决可变性检查问题的方案。 - Paul Cantrell

3

1) 在什么情况下,-copy返回一个不可变对象(如预期),在什么情况下返回一个可变对象?

您应该始终将其视为不可变的变体。不应使用返回类型的可变接口。除了优化之外,答案不应该有影响,并且应被视为实现细节,除非有记录。

显而易见的情况:由于许多原因,objc类族和类设计可能很复杂。返回可变副本可能仅仅是为了方便。

2) 如何实现获得“冻结”可变对象的预期效果,使其拒绝“冻结”?

使用不可变类的复制构造函数是一种好方法(类似于St3fan的回答)。像copy一样,它并不是一个保证。

我能想到的唯一原因是为了性能或强制执行受限界面(除非这是学术性的)。如果您想要性能或受限界面,则可以简单地封装该类型的实例,该实例在创建时进行复制并仅公开不可变接口。然后,您通过保留实现复制(如果这是您的意图)。

或者,您可以编写自己的子类并实现自己的变体副本。

最后,许多可变/不可变的Cocoa类纯粹是接口 - 如果需要确保特定行为,则可以编写自己的子类 - 但这非常不寻常。

也许有一个更好的描述为什么应该强制执行这一行为 - 现有的实现对于绝大多数开发人员/用途都很好。


1
请记住,没有一个copy实现 - 每个类都实现了自己的。而且,正如我们所知道的那样,Objective C运行时的实现在某些地方有点“松散”。因此,我认为我们可以说大多数情况下copy返回一个不可变版本,但也存在一些例外。
(顺便问一下,这是做什么用的:
NSArray *arr = [[NSMutable array] init];

?)


-1
将对象转换为可变对象的最佳方法是使用可变的“构造函数”。例如:
NSArray* array = ...;
NSMutableArray* mutableArray = [NSMutableArray arrayWithArray: array];

Copy 用于复制一个对象,而不是改变它的可变性。


要么发送mutableCopy消息(如果它实现了 NSMutableCopying),要么进行深拷贝,这两种方式都是获得可变副本的好方法。不过,这并不是提问者所询问的内容。 - Peter Hosey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接