Copy & MutableCopy

Copy & MutableCopy

前言

好久都没写东西了,除了平常的工作、学习,过年的喜庆与忙碌....都是借口。

废话不多说,让我们进行今天的话题,Copy 与 MutableCopy。


如何理解Copy

Question

为什么NSArray、NSString、NSDictionary常用copy修饰?

最近有一些同学问过我这个问题,为了弄清这个问题,可能涉及到的东西有点多,我会尽量用比较容易理解的例子和语言来解释。当然,实际编程中,可能会比我所举的例子要复杂的多。

1.什么是 immutable 和 mutable ?
  • 在Cocoa的世界中,有一些类分为不可变 (immutable) 和可变 (mutable) 两种版本。比如常见的 NSArray、 NSDictionary、 NSSet、NSString 等,对应关系为(immutable->mutable):
    NSArray -> NSMutableArray
    NSString -> NSMutableString
    NSDictionary-> NSMutableDictionary

** 在设计类的时候,我们应该充分运用属性来封装数据,而如果没有特殊要求,我们的属性对外公开,应该声明为只读,避免在一个类的外部直接修改这个类的属性,这样可以为我们避免很多麻烦的事情。 **

Example

** 我们都知道 NSSet 不会添加重复的数据。我们经常用这个类来实现一些功能,例如一个人的朋友这样的容器。那么,我们来看看,下面这段程序如何来打破 NSSet 的这个特性。 **

int main(int argc, const char * argv[]) {
 @autoreleasepool {
   NSMutableArray *array1 = [@[@1, @2] mutableCopy];
   NSMutableArray *array2 = [@[@1] mutableCopy];
   NSSet *set = [NSSet setWithObjects:array1, array2, nil];
   NSLog(@"Set: %@", set);
  }
  return 0;
}

** Log: **

2016-02-24 13:34:54.828 NSSet[1392:93361] Set: {(
      (
       1
   ),
       (
       1,
       2
   )
)}
Program ended with exit code: 0

** 很正常,没什么问题对吧? 看我稍稍加一句代码之后... **

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    NSMutableArray *array1 = [@[@1, @2] mutableCopy];
    NSMutableArray *array2 = [@[@1] mutableCopy];
    NSSet *set = [NSSet setWithObjects:array1, array2, nil];
    [array2 addObject:@2];
 NSLog(@"Set: %@", set);
  }
  return 0;
}

** Log: **

2016-02-24 13:38:11.294 NSSet[1409:94758] Set: {(
     (
     1,
     2
   ),
     (
     1,
     2
   )
)}
Program ended with exit code: 0

** 两个一毛一样的 NSMutableArray 就出现在一个 NSSet容器中了,改变了 NSSet 的原义!!!如果不信,大家可以动手试试。 **

** 说一个常见的问题吧,比如说我们常写的 CHRHomeViewController 有一个 badge 属性,这个属性可能会在 Discovery Profile 等多个模块中修改,如果我们直接改变这个 badge 属性,很可能就会出现 Race Condition(静态条件) 。如果我们把 badge 属性改成只读(readonly) ,然后公开一个 - updateBadge:(NSInteger)badge 方法,然后在本类的内部用队列或者其他技术,我们就可以比较容易的解决这个问题。 **

** 说了这么多,和我们最开始给出的问题有什么关系么? 关系还是有的,对于为什么 NSArray 等属性经常用 copy 来修饰,很大的一个方面就是为了保证,在set之后,我们得到的结果是 immutable 的类型。**

** 由于多态性,父类指针可以指向子类指针, 我们很可能写出这样的代码: **

定义一个User类,User有一个friends属性
@interface CHRUser : NSObject

@property (nonatomic, strong) NSSet <CHRUser *> *friends;

@end

@implementation CHRUser

- (instancetype)init
{
  self = [super init];
  if (self) {
    _friends = [NSSet set];
  }
  return self;
}

@end

// 我们从服务器上拉取用户chris的朋友们

int main(int argc, const char * argv[]) {
  @autoreleasepool {

    CHRUser *chris = [CHRUser new];

    NSMutableSet *friends = [NSMutableSet set];

    /* 从服务器拉取信息 ,解析得到这三个人是chris的朋友..... */
    CHRUser *mary = [CHRUser new];
    CHRUser *tom = [CHRUser new];
    CHRUser *bob = [CHRUser new];

    [friends addObject:mary];
    [friends addObject:tom];
    [friends addObject:bob];

    chris.friends = friends;

    NSLog(@"Chris' friends: %@", chris.friends);
    
    // 由于某些逻辑,在 friends 中又添加一个User
    CHRUser *babara = [CHRUser new];
    [friends addObject:babara];
    
    // 使用 friends 去做一些其他的事情
    NSLog(@"Chris' friends: %@", chris.friends);
  }
  return 0;
}

** 我们看看 Log 信息: **

2016-02-24 14:16:24.469 NSSet[1528:109524] Chris'   friends: {(
    <CHRUser: 0x100209ed0>,
    <CHRUser: 0x100209b80>,
    <CHRUser: 0x1002096b0>
)}
2016-02-24 14:16:24.469 NSSet[1528:109524] Chris'   friends: {(
    <CHRUser: 0x100500200>,
    <CHRUser: 0x100209b80>,
    <CHRUser: 0x100209ed0>,
    <CHRUser: 0x1002096b0>
)}
Program ended with exit code: 0

** Chris 的朋友莫名其妙的多了一个,什么情况 ? 因为 CHRUSer 的friends 属性使用的是 strong 关键字修饰,在 setFriends 之后,只是一个引用指向了 friends ,所以如果后面的 friends 进行增删改等操作,那么 chris.friends 就会跟着变咯。 **

** 我们把 friends 属性改用 copy 修饰, 再看 Log: **

2016-02-24 14:27:12.002 NSSet[1565:114822] Chris'   friends: {(
    <CHRUser: 0x1001037f0>,
    <CHRUser: 0x100102f20>,
    <CHRUser: 0x1001038c0>
)}
2016-02-24 14:27:12.003 NSSet[1565:114822] Chris'   friends: {(
    <CHRUser: 0x1001037f0>,
    <CHRUser: 0x100102f20>,
    <CHRUser: 0x1001038c0>
)}
Program ended with exit code: 0

** 嗯,这通常就是我们想要的结果了, NSDictionary、 NSString 等道理类似。可是为什么会这样呢? **
** 在 Cocoa 的世界中,因为诸如 NSArray 对应有 immutable 和 mutable 两种版本,所以 copy 操作就分成了 copy 和 mutableCopy 两种复制方式。其复制前后结果如下: **

  1. copy 前后的类型

NSArray copy -> NSArray
NSMutableArray copy -> NSArray
NSString copy -> NSString
NSMutableString copy -> NSString
.....


  1. mutableCopy 前后的类型

NSArray mutableCopy -> NSMutableArray
NSMutableArray mutableCopy -> NSMutableArray
NSString mutableCopy -> NSMutableString
NSMutableString mutableCopy -> NSMutableString
......

** 可以看出,经过 copy 操作后,得到的总是 immutable 类型, 经过 mutableCopy 操作后, 得到的总是 mutable 类型。 那么这两个操作到底做了什么呢? 代码看一下。 **

  1. 对 immutable 类进行 copy 和 mutableCopy 操作
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            CHRUser *user = [CHRUser new];
NSArray *array = @[user];

            NSArray *copiedArray = [array copy];
            NSMutableArray *mutableCopiedArray = [array mutableCopy];
        
            // 拷贝前
            NSLog(@"Before copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
      NSStringFromClass([array class]),
      array,
      array,
      array.firstObject);
      
            // copy 之后
            NSLog(@"After copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
      NSStringFromClass([copiedArray class]),
      copiedArray,
      copiedArray,
      copiedArray.firstObject);
        
            // mutableCopy 之后
            NSLog(@"After mutableCopy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
      NSStringFromClass([mutableCopiedArray class]),
      mutableCopiedArray,
      mutableCopiedArray,
      mutableCopiedArray.firstObject);
        }
        return 0;
    }

** Log: **

    2016-02-24 15:15:00.600 NSSet[1764:137701] Before copy : Class:__NSArrayI, Address: 0x1001038e0, Content: (
    "<CHRUser: 0x100102470>"
    ), ContentObject Address: 0x100102470
    2016-02-24 15:15:00.601 NSSet[1764:137701] After copy : Class:__NSArrayI, Address: 0x1001038e0, Content: (
    "<CHRUser: 0x100102470>"
    ), ContentObject Address: 0x100102470
    2016-02-24 15:15:00.602 NSSet[1764:137701] After mutableCopy : Class:__NSArrayM, Address: 0x100103a60, Content: (
    "<CHRUser: 0x100102470>"
    ), ContentObject Address: 0x100102470
    Program ended with exit code: 0

** 可以看出,对 immtable 类进行 copy 、 mutableCopy 得到的结果是不一样得, mutableCopy 之后我们得到了一个 NSArray 的可变版本,并且内存地址与原始的 array 不同,说明对 immutable 类进行 mutableCopy 得到的结果进行了内容拷贝,但是其容器内部的数据并没有拷贝内容(地址没有变化),也就是说,拷贝了一个 ; 而对 immutable 类进行 copy 操作,与原始的 array 是一毛一样, 也就是我们常说的,拷贝了一份指针。**

  1. 对 mutable 类进行 copy 和 mutableCopy 操作

     int main(int argc, const char * argv[]) {
       @autoreleasepool {
         CHRUser *user = [CHRUser new];
         NSMutableArray *mutableArray = [NSMutableArray arrayWithObject:user];
    
         NSArray *copiedArray = [mutableArray copy];
         NSMutableArray *mutableCopiedArray = [mutableArray mutableCopy];
         
         // 拷贝前
         NSLog(@"Before copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
       NSStringFromClass([mutableArray class]),
       mutableArray,
       mutableArray,
       mutableArray.firstObject);
       
         // copy 之后
         NSLog(@"After copy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
       NSStringFromClass([copiedArray class]),
       copiedArray,
       copiedArray,
       copiedArray.firstObject);
    
         // mutableCopy 之后
         NSLog(@"After mutableCopy : Class:%@, Address: %p, Content: %@, ContentObject Address: %p",
       NSStringFromClass([mutableCopiedArray class]),
       mutableCopiedArray,
       mutableCopiedArray,
       mutableCopiedArray.firstObject);
      }
       return 0;
     }
    

** 我们再来看下 Log **

    2016-02-24 15:21:59.657 NSSet[1814:141214] Before copy : Class:__NSArrayM, Address: 0x100509bc0, Content: (
    "<CHRUser: 0x100506dd0>"
    ), ContentObject Address: 0x100506dd0
    2016-02-24 15:21:59.658 NSSet[1814:141214] After copy : Class:__NSArrayI, Address: 0x100507450, Content: (
    "<CHRUser: 0x100506dd0>"
    ), ContentObject Address: 0x100506dd0
    2016-02-24 15:21:59.658 NSSet[1814:141214] After mutableCopy : Class:__NSArrayM, Address: 0x100509bf0, Content: (
    "<CHRUser: 0x100506dd0>"
    ), ContentObject Address: 0x100506dd0
    Program ended with exit code: 0

** 我们发现,对于一个 mutable 类型的对象来说,不论进行的是 copy 还是 mutableCopy 操作,其地址 Address 都不一样, 而其容器中的内容不变。则,对一个 mutable 类型的对象进行 copy 或者是 mutableCopy ,都拷贝了一个壳,而容器内的数据不会进行拷贝内容的操作。 **

** 所以说,我们经常对外开放的属性都是 immutable 的,但因为多态性,总有可能写出诸如 **

self.xxArray = xxMutableArray

** 这样的代码,程序可能会发生难以查明的 bug 。给维护带来灾难。也有的时候,有人会问你, 一个 NSMutableArray 使用 copy 修饰会发生什么, 经过上面的讲解, NSMutableArray copy 后得到的是一个 NSArray 类型的对象, 但是因为声明的是 NSMutableArray , 所以我们调用 addObject: 等 API 时, Xcode 并不会给我们警告,但是运行就会在 调用 addObject: 等 API 处奔溃(unrecognized selector sent to xxx)。 **

** 还有就是,我们要对自身编写的有可变版本的类同时实现两个协议来进行拷贝操作, NSCopying 和 NSMutableCopying 协议, 并在 **

  • (id)copyWithZone:(NSZone *)zone;
  • (id)mutableCopyWithZone:(NSZone *)zone;

** 两个方法分别返回不可变与可变版本,以供正常使用。 **

浅拷贝 VS 深拷贝

======

这里纯属扩充下知识哈,会的同学别吐槽...

什么是浅拷贝呢?

** 我们在对 immutable 类型的对象进行 copy 时,发现拷贝前后容器的地址不变,这种情况就叫做浅拷贝了。只拷贝一份指针,这个时候改变原数据之后,浅拷贝的数据会跟着变化,因为地址相同嘛。 **

什么是又是深拷贝呢?

** 深拷贝在我来看有一些不同的情况,比如说上面在对 immutable 类型的对象进行 mutableCopy 的时候,容器的地址变了,但是容器内容没变,这也算是深拷贝,不过只是拷贝了一个壳,如果改变其中的内容,那么拷贝后的容器的内容也跟着变化,这种拷贝我称之为 非完全深拷贝(自己起的名字,别喷,哎呀,大家别喷...)。 示例代码如下:**

我们在这里创建两个类,方便观察。一个是 CHRUser 类,有一个 name 读写属性, 和一个 dogs 的只读属性, 在这个类的内部存储着可变的 internalDogs 的读写属性,存储收养的狗狗们; CHRDog 类只有一个简单的 name 属性。 我们来看看如何实现 copy:

  • CHRUser

      @class CHRDog;
    
      @interface CHRUser : NSObject
    
      @property (nonatomic, copy) NSString *name;
      @property (nonatomic, copy, readonly) NSSet <CHRDog *> *dogs;
    
      - (void)adoptDog:(CHRDog *)dog;
    
      @end
    
      #import "CHRUser.h"
      
      #import "CHRDog.h"
    
      @interface CHRUser ()
    
      @property (nonatomic, strong, readwrite) NSMutableSet <CHRDog *> *internalDogs;
    
      @end
    
      @implementation CHRUser
    
      - (instancetype)init
      {
        self = [super init];
        if (self) {
          _internalDogs = [NSMutableSet set];
        }
        return self;
      }
    
      - (NSSet<CHRUser *> *)friends
      {
        return [_internalDogs copy];
      }
    
      - (void)adoptDog:(CHRDog *)dog
      {
        [_internalDogs addObject:dog];
      }
    
      - (NSString *)description
      {
        return [NSString stringWithFormat:@"Address: %p\n Name: %@\n InternalDogs: %@",
        self,
        _name,
        _internalDogs];
          }
    
      #pragma mark - NSCopying
      - (id)copyWithZone:(NSZone *)zone
      {
        CHRUser *copy = [[CHRUser allocWithZone:zone] init];
        copy.name = [_name copy];
        copy.internalDogs = [_internalDogs mutableCopy];
        return copy;
      }
    
      @end
    
  • CHRDog

      @interface CHRDog : NSObject
    
      @property (nonatomic, copy) NSString *name;
    
      @end
    
      @implementation CHRDog
      @end
    
  • main

      int main(int argc, const char * argv[]) {
        @autoreleasepool {
          CHRDog *dog = [CHRDog new];
          dog.name = @"A Dog";
    
          CHRUser *user = [CHRUser new];
          user.name = @"Chris";
          [user adoptDog:dog];
          CHRUser *copy = [user copy];
    
          CHRDog *anotherDog = [CHRDog new];
          anotherDog.name = @"Another";
          [user adoptDog:anotherDog];
    
          NSLog(@"%@", user);
          NSLog(@"%@", copy);
        }
        return 0;
      }
    
  • Log

      2016-02-24 18:51:43.102 NSSet[3794:250903] Address: 0x100109880
       Name: Chris
       InternalDogs: {(
          <CHRDog: 0x100100100>,
          <CHRDog: 0x1001098d0>
      )}
      2016-02-24 18:51:43.103 NSSet[3794:250903] Address: 0x10010bf70
       Name: Chris
       InternalDogs: {(
          <CHRDog: 0x1001098d0>
      )}
      Program ended with exit code: 0
    

** 可以看到, copy 前后,内部的变量地址相同,而且原 User 收养第二只狗狗的后, 对于 copy 复本没有影响。但是如果我改变了 internalDogs 的第一个 dog 的话,copy 复本的 internalDogs 还是会受到影响(这里我们用到了, mutable 类型 mutableCopy 后,拷贝了一层壳,还有对外公开只读的 dogs , 而在内部做可变存储的一些方式,解决Race Condition(静态条件)的方式并未给出,方式有很多,大家可以讨论下)。 **

** 这种结果有的时候就是我们想要的,但有的时候我们希望拷贝出来的副本与原始复本完全互相不影响,那么我们就需要用到 完全深拷贝(别打,别打.... 别走,喂,别走啊, 你还是打我吧
:( 。 **

  • 完全深拷贝

** 还有一种情况是,不只是拷贝了一个壳,连内容也全部拷贝过来,这种我称之为 完全深拷贝,就是将一个对象的容器属性、变量等一边拷贝,并且将容器中的每一内容都进行拷贝。还是上述代码,只需要改变一下User NSCopying 协议的方法实现,在让 CHRDog 实现NSCopying 协议,我们就可以做到完全深拷贝。 修改如下: **

  • 修改后的 CHRDog.m

      @implementation CHRDog
    
      - (id)copyWithZone:(NSZone *)zone
      {
        CHRDog *copy = [[CHRDog allocWithZone:zone] init];
        copy.name = _name;
        return copy;
      }
    
      @end
    
  • 修改后的 CHRUser - copyWithZone:

      - (id)copyWithZone:(NSZone *)zone
      {
        CHRUser *copy = [[CHRUser allocWithZone:zone] init];
        copy.name = [_name copy];
        copy.internalDogs = [[NSMutableSet alloc] initWithSet:_internalDogs copyItems:YES];
        return copy;
      }
    
  • Log

      2016-02-24 18:58:32.352 NSSet[3832:253844] Address: 0x1001021e0
       Name: Chris
       InternalDogs: {(
          <CHRDog: 0x1001024e0>,
          <CHRDog: 0x1001003c0>
      )}
      2016-02-24 18:58:32.352 NSSet[3832:253844] Address: 0x1001023b0
       Name: Chris
       InternalDogs: {(
          <CHRDog: 0x1001032e0>
      )}
      Program ended with exit code: 0
    

** 我们看到,修改之后的 Log 文件,原始的 user 与其复本 copy 中的 internalDogs 的地址都不一样, 这样,无论我们怎么修改原始的或者复本,他们都不会互相影响到了。 当然,在真正的项目中遇到的问题会比这复杂的多,不过只要我们掌握好原理,那么就容易解决了。 **

尾声

** 好久没有写新的东西了,今天花了一天的时间,由于时间的关系,后面写的可能有一些匆忙(中间还有人问了一些问题,导致思路断档...),不过只要大家觉得能够学到一些东西,或者我们共同讨论 ,升华一下知识,我都觉得无比的开心和荣耀。 **

** 文章中如果有不对的地方,请指出,我会尽可能快的修改,欢迎 大家来找茬 :)。 **

我是Chris, 一个 90 后的 iOSer。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 1. 属性中copy与strong特性的区别 在开始学习浅复制(Shallow Copy)、深复制(Deep Co...
    pro648阅读 1,697评论 2 16
  • 写在前面 Objective-C中的copy相关内容比我想象中要丰富多了。 NSCopying和NSMutable...
    梅西121阅读 1,681评论 0 0
  • 《那就这样吧》 26岁的我 却再也不能把这首歌听完 便化作 《惊鸿一面》 柳下闻谣琴起舞和一曲 仿若映当年翩若惊鸿...
    蒙山小道阅读 98评论 0 0
  • 1.日均运动大于10km(步行+快走+跑步) 2.记录每天的家庭流水,月底统计 3.写成功、做计划、多思考 4.每...
    资深小夏阅读 155评论 0 1
  • 在公路上发现花儿都开好了。 鲜艳的颜色,让司机不再那么视觉疲劳,减少交通事故,维护环境生态平衡。 这家店面太有复古...
    画心邪师阅读 327评论 2 3