NSString属性:复制还是保留?

336

假设我有一个名为SomeClass的类,它有一个string属性名称:

@interface SomeClass : NSObject
{
    NSString* name;
}

@property (nonatomic, retain) NSString* name;

@end

我知道如果将name赋值为NSMutableString,这可能导致错误行为。

  • 对于字符串,是否总是更好地使用copy属性而不是retain
  • "Copied"属性是否在任何方面都比"Retain-ed"属性低效?

6
后续问题:在dealloc中是否应该释放name - Chetan
7
@chetan 是的,应该这样做! - Jon
10个回答

442

对于类型为不可变值类且符合 NSCopying 协议的属性,您几乎总是应该在 @property 声明中指定 copy。在这种情况下,指定 retain 是您几乎永远不想要的。

以下是原因:

NSMutableString *someName = [NSMutableString stringWithString:@"Chris"];

Person *p = [[[Person alloc] init] autorelease];
p.name = someName;

[someName setString:@"Debajit"];

Person.name属性的当前值取决于该属性是声明为retain还是copy — 如果该属性标记为retain,则其值将为@"Debajit",但如果该属性标记为copy,则其值将为@"Chris"

由于在几乎所有情况下,您都希望防止在对象背后突变其属性,因此应将表示它们的属性标记为copy。(如果您自己编写setter而不是使用@synthesize,请记得在其中实际使用copy而不是retain。)


61
这个回答可能会有些混淆(参见http://robnapier.net/blog/implementing-nscopying-439#comment-1312)。关于NSString,你是完全正确的,但我认为你的观点有点太笼统了。NSString应该被复制的原因是它有一个常见的可变子类(NSMutableString)。对于没有可变子类的类(特别是你自己编写的类),通常最好保留它们而不是复制它们,以避免浪费时间和内存。 - Rob Napier
63
你的推理是错误的。你不应该根据时间/内存来决定复制还是保留,而应该基于所需的语义来确定。这就是为什么我在我的回答中特别使用了“不可变值类”的术语。这也不是一个类是否有可变子类,或者它本身是否可变的问题。 - Chris Hanson
10
Obj-C 不能通过类型来强制不可变,这与 C++ 的传递性 const 缺失相同。个人而言,我会假设字符串始终是不可变的。如果我需要使用可变字符串,我永远不会交出一个不可变引用,以防之后进行修改。我认为任何不同的行为都是代码异味。因此,在我的代码中(我独立开发时),我对所有字符串使用 retain。如果我是团队中的一员,我可能会有不同的想法。 - philsquared
5
@Phil Nash:我认为,在你独自工作的项目和与他人共享的项目中使用不同的风格是一种代码异味。在每种语言/框架中,都有开发人员达成共识的常规规则或样式。在私人项目中忽略它们似乎是错误的。至于你的理由“在我的代码中,我不返回可变字符串”:这可能适用于你自己的字符串,但你永远不知道从框架接收到的字符串情况如何。 - Nikolai Ruhe
7
@Nikolai,我只是不使用 NSMutableString,除了作为一个临时的 "字符串构建器" 类型(我会立即对其进行不可变复制)。我更喜欢它们是离散的类型,但我允许如果原始字符串不可变,则复制是免费的并且可以执行保留,这减轻了我的大部分担忧。 - philsquared
显示剩余11条评论

121

Copy 应该用于 NSString。如果它是可变的,那么就会被复制。如果它不可变,则只会被保留。这正是您希望在应用程序中得到的语义(让类型执行最佳操作)。


1
我仍然更喜欢可变和不可变形式是离散的,但我之前没有意识到如果原始字符串是不可变的,复制可能只是保留 - 这已经接近目标了。谢谢。 - philsquared
25
提到NSString属性声明为copy时,加一分(如果它是不可变的,当然会得到retain)。我能想到的另一个例子是NSNumber - matm
这个答案和@GBY的被踩的那个有什么区别? - Gary Lyn

68

对于所有字符串,使用复制属性而不是保留属性总是一个好主意吗?

是的,在一般情况下应该始终使用复制属性。

这是因为您的NSString属性可以传递NSString实例NSMutableString实例,因此我们无法确定正在传递的值是不可变对象还是可变对象。

"复制"属性是否在任何方面不如"保留"属性效率高?

  • 如果您的属性被传递了NSString实例,则答案是"" - 复制不比保留低效。
    (这并不低效,因为NSString足够聪明,不会实际执行复制操作。)

  • 如果您的属性被传递了NSMutableString实例,那么答案就是"" - 复制比保留低效。
    (这是因为必须进行实际的内存分配和复制,但这可能是一个理想的事情。)

  • 通常情况下,"复制"属性的效率可能会降低 - 但是通过使用NSCopying协议,可以实现一个类,其复制和保留一样高效。NSString实例就是一个例子。

通常(不仅仅是针对NSString),何时应该使用"复制"而不是"保留"?

当您不希望在没有警告的情况下更改属性的内部状态时,应始终使用copy。即使对于不可变对象 - 正确编写的不可变对象将有效地处理复制(有关不可变性和NSCopying的详细信息,请参见下一节)。

保留对象可能存在性能方面的原因,但这会导致维护开销加大——你必须处理内部状态在你的代码之外被更改的可能性。就像他们所说的——在最后优化。

但是,我编写了一个不可变的类 - 我能不能只是“retain”它?

不行——使用copy。如果你的类确实是不可变的,那么最好实现NSCopying协议,让你的类在使用copy时返回自身。如果你这样做:

  • 你的类的其他用户在使用copy时将获得性能的好处。
  • copy注解使你自己的代码更易于维护——copy注解表明你真的不需要担心这个对象的状态在别处发生改变。

40

我尝试遵循这个简单的规则:

  • 在我将对象分配给属性时,我想要保留其值的时间点吗?使用copy

  • 想要持有对象,而且我不关心它当前或未来的内部值是什么?使用strong(retain)。

举例来说:我是想要持有“Lisa Miller”这个名字copy),还是我想要持有人物Lisa Miller (strong)?她的名字可能会在以后更改为“Lisa Smith”,但她仍然是同一个人。


14

通过这个示例,可以说明复制和保留的概念:

NSMutableString *someName = [NSMutableString stringWithString:@"Chris"];

Person *p = [[[Person alloc] init] autorelease];
p.name = someName;

[someName setString:@"Debajit"];
如果属性的类型为copy,那么将会创建一个新的副本来保存someName字符串的内容,并赋值给[Person name]字符串。现在对于someName字符串的任何操作都不会影响[Person name]字符串。
[Person name]和someName字符串将拥有不同的内存地址。
但是如果属性的类型为retain,
那么[Person name]将会持有与somename字符串相同的内存地址,只是somename字符串的引用计数会增加1。
因此,对somename字符串进行的任何更改都将反映在[Person name]字符串中。

3

在声明属性时加上“copy”显然违背了使用面向对象环境的原则,因为堆上的对象是按引用传递的,这里的一个好处是当更改对象时,所有对该对象的引用都会看到最新的更改。许多语言提供“ref”或类似的关键字,以允许值类型(即堆栈中的结构)受益于相同的行为。个人而言,我会谨慎使用“copy”,如果我觉得属性值应该受到分配对象所做更改的保护,我可以在分配期间调用该对象的复制方法,例如:

p.name = [someName copy];

当然,在设计包含该属性的对象时,只有您知道是否从赋值拷贝模式中获益 - Cocoawithlove.com 的文章如下所述:
“当setter参数可能是可变的但是您不能让属性的内部状态在没有警告的情况下发生更改”-因此,判断您是否能够承受值意外更改完全取决于您自己。想象一下这种情况:
//person object has details of an individual you're assigning to a contact list.

Contact *contact = [[[Contact alloc] init] autorelease];
contact.name = person.name;

//person changes name
[[person name] setString:@"new name"];
//now both person.name and contact.name are in sync.

在这种情况下,如果不使用复制,我们的联系对象会自动采用新值;但是,如果使用它,我们必须手动确保更改被检测并同步。在这种情况下,保留语义可能是可取的;在另一种情况下,复制可能更合适。

1
@interface TTItem : NSObject    
@property (nonatomic, copy) NSString *name;
@end

{
    TTItem *item = [[TTItem alloc] init];    
    NSString *test1 = [NSString stringWithFormat:@"%d / %@", 1, @"Go go go"];  
    item.name = test1;  
    NSLog(@"-item.name: point = %p, content = %@; test1 = %p", item.name, item.name, test1);  
    test1 = [NSString stringWithFormat:@"%d / %@", 2, @"Back back back"];  
    NSLog(@"+item.name: point = %p, content = %@, test1 = %p", item.name, item.name, test1);
}

Log:  
    -item.name: point = 0x9a805a0, content = 1 / Go go go; test1 = 0x9a805a0  
    +item.name: point = 0x9a805a0, content = 1 / Go go go, test1 = 0x9a84660

0

在声明NSString属性时,您应该始终使用copy

@property (nonatomic, copy) NSString* name;

您应该阅读以下内容,以获取有关它是否返回不可变字符串(如果传递了可变字符串)或返回保留字符串(如果传递了不可变字符串)的更多信息

NSCopying协议参考

通过保留原始对象而不是在类及其内容不可变时创建新副本来实现NSCopying

值对象

因此,对于我们的不可变版本,我们只需这样做:

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

-1
如果字符串很大,复制将影响性能,两个大字符串的副本将使用更多的内存。

-1

由于名字是一个(不可变的)NSString,如果你将另一个NSString设置为名字,那么复制或保留没有任何区别。换句话说,复制的行为就像保留一样,增加了一个引用计数。我认为这是对不可变类的自动优化,因为它们是不可变的,不需要被克隆。但是当一个NSMutalbeString mstr被设置为名字时,mstr的内容将被复制以确保正确性。


1
你正在混淆已声明类型和实际类型。如果你使用"retain"属性并分配一个NSMutableString,那个NSMutableString将会被保留下来,但依旧可以被修改。如果你使用"copy",当你分配一个NSMutableString时,将会创建一个不可变的副本;从那时起,在该属性上使用"copy"只是保留,因为可变字符串的副本本身是不可变的。 - gnasher729
1
如果您使用来自保留变量的对象,则缺少一些重要信息,当该变量被修改时,您的对象也会被修改;如果它来自复制的变量,则您的对象将具有该变量的当前值,该值不会改变。 - Bryan P

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