属性修饰词Copy的那些坑

引言

OC里有一个Protocol叫做NSCopying,它声明了一个必须要实现的方法- (id)copyWithZone:(nullable NSZone *)zone;
假设有一个Person类,如果要实现这个协议的话,我们一般是这么写的。

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

@implementation Person

- (id)copyWithZone:(nullable NSZone *)zone {
    Person *copy = [[Person allocWithZone:zone] init];
    copy->_name = _name.copy;
    return copy;
}

@end

在方法里我们创建了一个Person对象,被将当前的_name作为实例变量传给了这个新建的对象,最后将这个对象返回,最后这样就实现了copy。

Copy作为属性修饰词的那些坑

  • 被修饰的属性必须要实现NSCopying,否则crash。
  • Copy不一定是Copy
  • Foundation的immutable类如NSArray,NSString在使用Copy时等同于retain或strong。

先看下面的代码

NSString *str1 = @"string";
NSString *str1Copy = str1.copy;
NSLog(@"str1:%p, str2:%p", str1, str1Copy);
        // str1:0x100001058, str2:0x100001058
        
        NSMutableString *mStr1 = [[NSMutableString alloc] initWithString:str1];
        NSMutableString *mStr1Copy = mStr1.copy;
        NSLog(@"\nmStr1 class = %@, address = %p\nmStr1Copy class = %@, address = %p", mStr1.class, mStr1, mStr1Copy.class, mStr1Copy);
        // mStr1     class = __NSCFString,          address = 0x100500230
        // mStr1Copy class = NSTaggedPointerString, address = 0x676e6972747365

        @try {
            [mStr1Copy appendString:@"a"];
        } @catch (NSException *exception) {
            NSLog(@"%@", exception);
            // -[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0x676e6972747365
        }
        
        NSArray *arr = @[@1, @2, @3];
        NSArray *arrCopy = arr.copy;
        NSLog(@"arr:%p, arrCopy:%p", arr, arrCopy);
        // arr:0x100202dd0, arrCopy:0x100202dd0
        
        NSMutableArray *mArr = [NSMutableArray arrayWithArray:arr];
        NSMutableArray *mArrCopy = mArr.copy;
        NSLog(@"\nmArr class = %@, address = %p\nmArrCopy class = %@, address = %p", mArr.class, mArr, mArrCopy.class, mArrCopy);
        // mArr     class = __NSArrayM, address = 0x100200510
        // mArrCopy class = __NSArrayI, address = 0x100202270

Foundation的NSArray,NSString和它们对应的mutable在使用copy时是与我们期待的行为不同的。

  • immutable对象因为是不可变的,我们需要copy一个对象是因为需要对这个copy进行一定的改变,例如我copy一个文件,然后修改这个文件,保持源文件不变。既然对象本身不可变的,即使你copy出来也是不能改的,那么为什么还需要在内存里再塞一个一模一样的不可变对象呢?
  • mutable对象在实现时也不是我们期待的copy行为,虽然这次的确创建了一个全新的对象,与immutable不同。但是这个新的对象除了在内容上和原来的对象一样外,但是它们的class却是不同的。mutable对象在实现copy方法时实际上是创建了一个immutable对象并返回。

上面两点印证了,Copy不一定是Copy。值得注意的是,因为dynamic typing的关系,对象实际的类型是在运行时决定而不是编译时决定的。所以NSMutableArray *mArrCopy = mArr.copy;是完全没有问题的,你甚至可以写任何NSObject的子类。但是到了运行时就会原形毕露,所以这里会报错[mStr1Copy appendString:@"a"];

在看看我们自己实现的NSCopying吧,这正是我们所期待的的copy,两个对象地址不同,内容相同,你可以随便修改一个而不影响另一个。

        Person *person = Person.new;
        person.name = @"Noah";
        
        Person *person2 = person.copy;
        NSLog(@"\nperson address = %p name = %@\nperson2 address = %p name = %@", person, person.name, person2, person2.name);
        // person  address = 0x100203c40 name = Noah
        // person2 address = 0x100202820 name = Noah

NOTE:
因为NSCopying只是一个Protocol,所以没什么阻止你乱来,你可以乱七八糟的返回一些让其他人困惑的东西。所以在开发时,不要假定copy的行为!除了系统的那些是已知的外,第三方或其他人实现的记得要看源码。

上面说了那么多,现在终于入正题,作为修饰词的用法。
关于实例变量,setter和getter这里不多说,看这里http://www.jianshu.com/p/e442711a867a

假设有一个类有一个属性Person
@property (nonatomic, copy) Person *person;
当属性被copy修饰后,编译器生成的setter大概是这样的

- (void)setPerson:(Person *)person {
    _person = person.copy;
}

用strong的话是这样的

- (void)setPerson:(Person *)person {
    _person = person;
}

实际上作为修饰词,copy没有什么特别的。它只是在setter里加了个copy,只要属性实现了NSCopying。所以为什么在没有实现NSCopying的属性上使用时会崩溃了。

NOTE:
修饰immutable对象时必须要使用copy。这个面试经常问到,很多人也无法准确说出。
前面说过因为dynamic typing的关系,对象的类型是在运行时决定的。所以str在运行时会是NSMutableString类型。如果开发者本人并不知情并继续使用下去的话就会出现问题了。因为运行时的类型并不是期待的类型。

    NSString *str = nil;
    NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"str2"];
    str = mStr;

解决方法也很简单,对可变对象copy一下就好了。str = mStr.copy

关于mutableCopy

这个东西不能作为属性修饰词,但这里也有必要说说。
继承自NSObject的类都有copy和mutableCopy两个方法。当调用这两个方法后,系统会自动调用- (id)copyWithZone:(nullable NSZone *)zone- (id)mutableCopyWithZone:(nullable NSZone *)zone;
两个方法除了名字外都一样,所以看你怎么实现。

系统的immutable或mutable类都实现了NSCopying和NSMutableCopying,下面是一些特点。

  • 对可变对象进行copy,返回的会是一个不可变对象,并不是一般意义上的copy行为,因为copy后的对象类型不同了。
  • 对可变对象进行mutableCopy,返回的是另一个可变对象,这种才是copy行为,两个对象内容一样,类型一样,地址不同。
  • 对不可变对象进行copy,系统不会再创建对象,而是直接返回源对象地址。
  • 对不可变对象进行mutableCopy则返回一个可变对象,同样不是一般意义上的copy。
  • 在不可变对象上实现一般意义上的copy的方式是immutable.mutableCopy.copy

NOTE:
本文重点是,NSCopying是一个protocol,所以不要假定它实现了一个copy行为,系统实现的就不是我们期待的了。当然,在我们实现时,最好还是按一般的行为来实现,这样才不会让别人困惑。

你可能感兴趣的:(属性修饰词Copy的那些坑)