使用“copy”属性特性来维护不可变的NSString。

7
我对iOS开发和Objective-C编程非常陌生。我一直在做应用程序开发库上的练习。
这是我正在尝试理解的当前练习。 3. 测试如果将可变字符串设置为人的名字,然后在调用修改后的sayHello方法之前改变该字符串会发生什么。通过添加copy属性来更改NSString属性声明并再次测试。
我尝试这样做,但是即使使用了复制属性,我修改的NSString实际上也会发生变化。
以下是我的声明和实现以及我的测试代码。
XYZPerson.h
#import <Foundation/Foundation.h>

@interface XYZPerson : NSObject

@property (copy) NSString *firstName;
@property NSString *lastName;
@property NSDate *dob;

- (void)sayHello;
- (void)saySomething:(NSString *)greeting;

+ (id)init;
+ (id)personWithFirstName:(NSString *)firstName lastName:(NSString *)lastName dob:(NSDate   *)dateOfBirth;


@end

//XYZPerson.m
#import "XYZPerson.h"

@implementation XYZPerson

@synthesize firstName = _firstName;
@synthesize lastName = _lastName;
@synthesize dob = _dob;


- (void)sayHello {
    [self saySomething:@"Hello World!"];
    NSLog(@"This is %@ %@", self.firstName, self.lastName);
}

- (void)saySomething:(NSString *)greeting {
    NSLog(@"%@", greeting);
}

+ (id)init {
    return [self personWithFirstName:@"Yorick" lastName:@"Robinson" dob:8/23/1990];
}

+ (id)personWithFirstName:(NSString *)firstName lastName:(NSString *)lastName dob:(NSDate   *)dateOfBirth{
    XYZPerson *person = [[self alloc] init];
    person.firstName = firstName;
    person.lastName = lastName;
    person.dob = dateOfBirth;

    return person;
}

@end

//Test code
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "XYZPerson.h"
#import "XYZShoutingPerson.h"


int main(int argc, char *argv[])
{
    @autoreleasepool {
        XYZPerson *guy = [XYZPerson init];
        [guy sayHello];

        //I thought that this change would never be made, but it is everytime I run the code.
        guy.firstName = @"Darryl";
        [guy sayHello];

        XYZShoutingPerson *girl = [XYZShoutingPerson init];
        [girl sayHello];
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

你能澄清一下你的问题是什么吗? - SnareChops
是的,当然。我所读到的有关copy属性的所有内容都表明,设置为该属性的属性不会随传递对象中的更改而更改。在我的代码中,我认为修改guy对象的'firstName'属性将没有影响,因为该属性是使用copy属性声明的。 - smitty
3个回答

9
考虑以下较短的示例(在CodeRunner中运行):
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,strong) NSString *name; // strong should be copy
@end

@implementation Person
@end

int main(int argc, char *argv[]) {
    @autoreleasepool {
        Person *p = [Person new];

        NSMutableString *name = [[NSMutableString alloc] initWithString:@"Alice"];
        p.name = name;
        NSLog(@"%@",p.name); // prints Alice

        [name appendString:@"xxx"];
        NSLog(@"%@",p.name); // prints Alicexxx
    }
}

我将名称指向可变字符串,然后添加一些字符。结果,该名称已在对象内部更改。但是,如果在声明属性时使用copy替换strong,则会为Person对象创建一个新的不可变字符串。
故事的寓意是,使用copy可以防止当某人传递一个对象并且该对象发生更改时产生副作用。
当传递可变字符串(NSMutableString)时,消息-[NSString copy]会生成一个副本;当传递不可变字符串(NSString)时,它会保留。因此,在声明NSString属性时始终要使用copy:
@property (nonatomic,copy)   NSString *string;  // OK
@property (nonatomic,strong) NSString *string;  // strong should be copy 

谢谢你的帮助,伙计。这让我以不同的方式看待了这个问题。但是我仍然有点困惑。你能否给出一个简短(可能更实际)的例子,说明使用这个属性确实可以防止相关数据被损坏? - smitty
有些人甚至认为,任何具有可变子类的不可变对象都应该声明为-copy。 - CodaFi
@smitty 一个例子是当有人可能会在你脚下拉地毯的任何情况。例如,NSMutableDictionary 是一个哈希表,也就是一个数组,其中索引是通过将键转换为数字并将其映射到数组位置来计算的。如果您在一个键上存储一个对象,然后更改该键,那么该对象突然就在错误的数组位置(因为新键在数组中映射到其他位置)。防止您的类的客户端执行此类可怕操作的更简单方法是让 NSMutableDictionary 复制键。 - Jano
好的,那很有道理。我从来没有这样想过。正如我所说,我在Objective-C编程方面非常新手,所以我还没有遇到过任何类似的编码问题。谢谢你的帮助。 - smitty
太好了!当我看到很多教程和书籍中像这个例子一样将NSString指针彼此赋值时,我感到困惑。我进行了一个实验,与你的实验相同,但也包括了一个NSMutableString作为属性,并得出了相同的结论。 - Dezzo

2
我在做同样的书时遇到了这个问题。我添加了复制并且完全相同的事情发生了,当我向用于firstName的NSMutableString变量追加内容时,它仍然会发生变异。然后我读了这一节:
如果您需要直接设置copy属性的实例变量(例如在初始化方法中),请不要忘记设置原始对象的副本:
-(id)initWithSomeOriginalString:(NSString *)aString { self = [super init]; if (self) { _instanceVariableForCopyProperty = [aString copy]; } return self; }
因此,我回到我的XYZPerson.m并查看了我的init代码。
我改变了:
- (id)initWithFirstName:(NSMutableString *)aFirstName lastName:(NSString *)aLastName
        dateOfBirth:(NSDate *)aDate    {
self = [super init];

if (self) {
    _firstName = aFirstName;
    _lastName = aLastName;
    _dateOfBirth = aDate;
}

return self;

}

To:

- (id)initWithFirstName:(NSMutableString *)aFirstName lastName:(NSString *)aLastName
        dateOfBirth:(NSDate *)aDate    {
self = [super init];

if (self) {
    _firstName = [aFirstName copy];
    _lastName = aLastName;
    _dateOfBirth = aDate;
}

return self;

然后,瞬间变化:它以正确的方式工作了!它复制了我之前使用的NSMutableString,当我在方法调用之前将其末尾附加了内容时,它不会发生变化。


0

我觉得你误解了"copy"的作用。

NSMutableString *string = [NSMutableString stringWithString:@"test"];

XYZPerson *guy = [XYZPerson init];
guy.firstName = string;
guy.lastName = string;

[string replaceCharactersInRange:NSMakeRange(1, 1) withString:@"x"];

[guy sayHello];

输出

This is test txst

在这个例子中,firstName 是拷贝的,因此当可变字符串 string 改变时不会发生变化,lastName 不是拷贝,所以它的值会在可变字符串 string 改变时也改变。
这里发生的情况是lastNamestring是同一个对象,所以当更改string时,lastName会作为副作用而被更改。这被认为是非常糟糕的,你永远不希望出现这种行为。使用复制确保firstNamestring是不同的对象,对string的更改不能影响firstName

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