属性修饰词Copy的那些坑

作者: Noah1985 | 来源:发表于2016-12-08 18:15 被阅读351次

    引言

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

    @interface Person: NSObject <NSCopying>
    @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行为,系统实现的就不是我们期待的了。当然,在我们实现时,最好还是按一般的行为来实现,这样才不会让别人困惑。

    相关文章

      网友评论

      • 溪枫狼:想通了,关键看怎么实现协议方法。谢谢指点,不然一直掉坑里了
      • 黑白灰的绿i:你是cocoa China那个吗 一样的头像一样的名字

      本文标题:属性修饰词Copy的那些坑

      本文链接:https://www.haomeiwen.com/subject/fbmsmttx.html