原型模式

本文概念主要摘取书籍**《大话设计模式》**, ObjC的深浅拷贝主要是对**《苹果开发文档 》**相关内容的理解和翻译,以及写代码进行验证. 文章中如有错误,请以原书以及官方文档为准。

原型模式 (Prototype):用原型对象实例制定创建对象的种类,并且通过拷贝这些原型创建新的对象.

![image-20200717144712499](/Users/liuxiaoyong/Library/Application Support/typora-user-images/image-20200717144712499.png)

原型模式其实就是从一个对象再创建另外一个可定制的对象, 而且不需要知道任何创建的细节.

代码如下 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person implements Cloneable {
    int age;
    String name;

    Location location;
		
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Location {
    String city;
    int no;

    public Location(String city, int no) {
        this.city = city;
        this.no = no;
    }
}

public class Test {

    public static void main(String[] args) throws Exception{

        Person p1 = new Person(8, "Jack", new Location("北京", 110));
        Person p2 = (Person) p1.clone();

        p2.age = 10;
        p2.name = "Rose";
        p2.location.city = "石家庄";
        p2.location.no = 310;
        System.out.println(p1);	// Person{age=8, name='Jack', location=Location{city='石家庄', no=310}}
        System.out.println(p2); // Person{age=10, name='Rose', location=Location{city='石家庄', no=310}}
        System.out.println(p1 == p2); // false
        System.out.println(p1.location == p2.location); // true
    }
}
  • Person类, 实现 Cloneable协议, 并且实现 clone() 方法
  • 就实现了一个简单的原型(clone)模式
  • 但是查看输出后,发现修改 p2.location之后, p1.location也被改变,p2 和 p1两个对象互相影响了.
  • 为什么会这样? 就涉及到了深复制,浅复制的概念.

深复制、浅复制

  • Person 类中的 int age, String name 都是值引用, 而 Location location 是地址引用
  • super.clone()方法是这样
    • 如果字段是值类型,则对该字段执行逐位复制.
    • 如果字段是地址类型, 则复制引用不复制引用的对象.
  • 因此, p1 和 p2的location指向的是同意对象
  • 浅复制 : 被复制的对象的所有变量都含有与原来对象相同的值, 而所有的对其他对象的引用都仍然指向原来的对象.
  • 深复制 : 对其他对象的引用变量也复制成新的对象,而不是引用原有的对象.
  • 如何实现 location深复制
    • Location类 也实现 Cloneable 接口,并实现 clone() 方法
    • 且在 Person类的 **clone()**方法中, clone一份 location 对象,并将clone后的对象复制给新的person
  • 代码如下 :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Person implements Cloneable {
    int age = 8;
    int score = 100;

    Location location;
    protected Object clone() throws CloneNotSupportedException {
        Person person = (Person)super.clone();
        person.location = (Location)location.clone();
        return person;
    }
}

class Location implements Cloneable {
    String city;
    int no;
		
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

深复制后,运行上面同样的测试代码,输出结果就不同了。

Person{age=8, name=Jack, location=Location{city='北京’, no=110}} Person{age=10, name=Rose, location=Location{city='石家庄’, no=310}} false false

  • 可以看出, 修改p2的 location 之后,没有影响 p1。 且 p1.location == p2.location 输出 false。
  • 说明 p1 和 p2的location 没有指向相同对象, 实现了 location对象的深复制
  • 当对象中,又有子对象,子对象又有对象属性时呢?
    • 这种情况,使用时要考虑好深复制要复制多少层,而且要当心出现循环引用。

Apple Developer Documentation

Copying Collections

There are two kinds of object copying: shallow copies and deep copies. The normal copy is a shallow copy that produces a new collection that shares ownership of the objects with the original. Deep copies create new objects from the originals and add those to the new collection. This difference is illustrated by Figure 1.

  • 对象复制有两种类型 : 浅复制和深复制.
  • 普通的复制是浅复制,它会生成一个新的集合,新集合和原集合共同享有对象的所有权。
  • 深复制从原集合中拷贝所有对象,并新对象其添加到新集合中。

Figure1 展示了 浅拷贝 和 深拷贝的区别.

Figure 1 Shallow copies and deep copiesimg

Shallow Copies

There are a number of ways to make a shallow copy of a collection. When you create a shallow copy, the objects in the original collection are sent a retain message and the pointers are copied to the new collection. Listing 1 shows some of the ways to create a new collection using a shallow copy.

  • 有很多方法可以对集合进行浅复制。 当你创建一个浅复制对象时。原集合中的对象会被发送一个 retain 消息, 并且指针将被复制到新集合中.

Listing 1展示了一些浅复制的方法.

Listing 1 Making a shallow copy

NSArray *shallowCopyArray = [someArray copyWithZone:nil];
NSDictionary *shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:NO];
1
2
3
4
Cat *cat = [[Cat alloc] initWithName:@"🐱"];
Person *obj = [[Person alloc] initWithName:@"Jack" gender:@"male" cat:cat];
NSArray *arr1 = @[obj];
NSArray *arr2 = [arr1 copyWithZone:nil];
1
2
3
4
Cat *cat = [[Cat alloc] initWithName:@"🐱"];
Person *obj = [[Person alloc] initWithName:@"Jack" gender:@"male" cat:cat];
NSArray *arr1 = @[obj];
NSArray *arr2 = [[NSArray alloc] initWithArray:arr1 copyItems:NO];

验证这两种情况, 都是浅拷贝, 两个数组中 Person对象的内存一样.

屏幕快照 2020-07-17 下午5.36.17

These techniques are not restricted to the collections shown. For example, you can copy a set with the copyWithZone: method—or the mutableCopyWithZone: method—or an array with initWithArray:copyItems: method.

  • 这些技术不仅限于所示的集合。例如,你可以使用 copyWithZone `mutableCopyWithZone拷贝集合
  • 也可以使用 initWithArray:copyItems 复制数组.

Deep Copies

There are two ways to make deep copies of a collection. You can use the collection’s equivalent of initWithArray:copyItems: with YES as the second parameter. If you create a deep copy of a collection in this way, each object in the collection is sent a copyWithZone: message. If the objects in the collection have adopted the NSCopying protocol, the objects are deeply copied to the new collection, which is then the sole owner of the copied objects. If the objects do not adopt the NSCopyingprotocol, attempting to copy them in such a way results in a runtime error. However, copyWithZone: produces a shallow copy. This kind of copy is only capable of producing a one-level-deep copy. If you only need a one-level-deep copy, you can explicitly call for one as in Listing 2.

  • 有两种方法可以深拷贝集合. 你可以使用集合的等效项 initWithArray:copyItems: 并以 YES作为 第二个参数
  • 如果你用这种方式创建集合的深拷贝副本, 则集合中的每个对象都会收到 copyWithZone: 消息。
  • 如果集合中的对象已实现 NSCopying 协议,则将对象深复制到集合中,新集合是被复制对象的唯一所有者。
  • 如果集合中的对象没有实现 NSCopying协议, 则以这种方式复制会导致运行时错误.
  • 然而, copyWithZone 也会产生浅拷贝. 这种拷贝方式值能生成 一级深拷贝副本。 如果你只需要一个一级深拷贝副本, 你可以显示的调用清单2所示的方法.

Listing 2 Making a deep copy

1
NSArray *deepCopyArray=[[NSArray alloc] initWithArray:someArray copyItems:YES];
1
2
3
4
Cat *cat = [[Cat alloc] initWithName:@"🐱"];
Person *obj = [[Person alloc] initWithName:@"Jack" gender:@"male" cat:cat];
NSArray *arr1 = @[obj];
NSArray *arr2 = [[NSArray alloc] initWithArray:arr1 copyItems:YES];

屏幕快照 2020-07-17 下午5.39.48

执行结果如上图 :

  • 两个数组中 peson 对象地址不一样
  • 而两个person 对象的 cat 内存地址一样
  • 所以结论是 : 产生了深拷贝, 且是 一级深拷贝, 第二级别没有仍然是浅拷贝

This technique applies to the other collections as well. Use the collection’s equivalent of initWithArray:copyItems: with YES as the second parameter.

  • 这项技术也适用于其他集合,使用集合的等效项 initWithArray:copyItems: 并以 YES 作为第二个参数.

If you need a true deep copy, such as when you have an array of arrays, you can archive and then unarchive the collection, provided the contents all conform to the NSCoding protocol. An example of this technique is shown in Listing 3.

  • 如果你需要一个真正的深复制副本(假如,当你有一个二维数组时).

  • 你可以通过归档再接档该集合. 如果其中对象全部都实现 NSCoding协议.

  • Listing3 展示了此技术的一个示例.

Listing 3 A true deep copy

NSArray* trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

Copying and Mutability

When you copy a collection, the mutability of that collection or the objects it contains can be affected. Each method of copying has slightly different effects on the mutability of the objects in a collection of arbitrary depth:

  • copyWithZone: makes the surface level immutable. All deeper levels have the mutability they previously had.

  • initWithArray:copyItems: with NO as the second parameter gives the surface level the mutability of the class it is allocated as. All deeper levels have the mutability they previously had.

  • initWithArray:copyItems: with YES as the second parameter gives the surface level the mutability of the class it is allocated as. The next level is immutable, and all deeper levels have the mutability they previously had.

  • Archiving and unarchiving the collection leaves the mutability of all levels as it was before.

  • 当复制集合时, 该集合或其包含对象的可变性可能会收到影响.

  • 每种复制方法对任意深度的集合中对象的可变性略有不同.

    • copyWithZone : 使表层不变, 深层次的对象维持其之前的可变性

    • initWithArray:copyItems: 当第二个参数为 NO 时, 使表层具有其类的可变性。所有更深层次的元素都具有以前的可变性

      • 代码示例 :
      • arr2 的可变性有 [NSArray alloc] 的类型确定, 如果用 NSArray 初始化, 则 arr2不可变
      • 使用 NSMutableArray 初始化, 则 arr2 可变
      1
      2
      
      NSArray *arr1 = @[@"1",@"2",@"3"];
      NSMutableArray *arr2 = [[NSArray alloc] initWithArray:arr1 copyItems:NO];
      
    • initWithArray:copyItems: 当第二个参数位 YES时, 使表层具有其类的可变性。 下一级别是不可变的,并且所有更深层次的元素都具有其以前的可变性

  • 归档 / 接档 将保留所有级别元素的可变性。

非容器类对象

NSString的拷贝

  • 自定义对象的 copyWithZone: 方法,是我们自己实现,我们清楚其是深拷贝还是浅拷贝
  • 但是 NSString 的 copy 是深拷贝还是浅拷贝呢 ?
  • 不能看源代码,只能代码验证咯
验证
  • 不可变字符串,copy -> 浅拷贝
1
2
3
4
5
NSString *str1 = @"I am a String";
NSString *str2 = str1.copy;

NSLog(@"%@,<memory address: %p>", str1, str1);
NSLog(@"%@,<memory address: %p>", str1, str2);
1
2
测试[60501:1063290] I am a String,<memory address: 0x1000020a8>
测试[60501:1063290] I am a String,<memory address: 0x1000020a8>
  • 可变字符串, copy -> 深拷贝
1
2
3
4
5
NSMutableString *str1 = [NSMutableString stringWithString:@"I am a String"];
NSString *str2 = str1.copy;

NSLog(@"%@,<memory address: %p>", str1, str1);
NSLog(@"%@,<memory address: %p>", str1, str2);
1
2
测试[60516:1064232] I am a String,<memory address: 0x100608d40>
测试[60516:1064232] I am a String,<memory address: 0x100608e00>
  • 不可变字符串, mutableCopy -> 深拷贝
1
2
3
4
5
NSString *str1 = @"I am a String";
NSString *str2 = str1.mutableCopy;

NSLog(@"%@,<memory address: %p>", str1, str1);
NSLog(@"%@,<memory address: %p>", str1, str2);
1
2
测试[60527:1065196] I am a String,<memory address: 0x1000020a8>
测试[60527:1065196] I am a String,<memory address: 0x101852a10>
  • 可变字符串, mutableCopy -> 深拷贝
1
2
3
4
5
NSMutableString *str1 = [NSMutableString stringWithString:@"I am a String"];
NSString *str2 = str1.mutableCopy;

NSLog(@"%@,<memory address: %p>", str1, str1);
NSLog(@"%@,<memory address: %p>", str1, str2);
1
2
测试[60539:1066040] I am a String,<memory address: 0x100554890>
测试[60539:1066040] I am a String,<memory address: 0x100554950>
结论

通过以上代码分析,可以得出结论 :

  • [immutableObj copy] -> 浅复制
  • [immutableObj mutableCopy] -> 深复制
  • [mutableObj copy] -> 深复制
  • [mutableObj copy] -> 深复制

自定义对象的拷贝

  • 在ObjC中,使用原型模式创建自定义对象, 比如 Person类

    • 首先需要遵守 NSCopying协议

      1
      
      @interface Person : NSObject <NSCopying>
      
    • 实现 copyWithZone: 方法

      1
      2
      3
      4
      
      - (id)copyWithZone:(nullable NSZone *)zone{
          Person *copyObj = [[[self class] allocWithZone:zone] initWithName:_name gender:_gender];
          return copyObj;
      }
      
    • 这时,我们就可以利用 copy 来创建 Person 对象了

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              Person *p1 = [[Person alloc] initWithName:@"Jack" gender:@"male"];
              Person *p2 = p1.copy;
                  
              p2.name = @"Rose";
              p2.gender = @"female";
                  
              NSLog(@"%@",p1);
              NSLog(@"%@",p2);
          }
          return 0;
      }
      
    • 我们覆写了 description 方法,最终打印结果如下 :

      1
      2
      3
      
      - (NSString *)description{
          return [NSString stringWithFormat:@"%@ is %@, <memory address: %p>", _name, _gender, self];
      }
      
      1
      2
      
      测试[60329:1050217] Jack is male, <memory address: 0x10059d840>
      测试[60329:1050217] Rose is female, <memory address: 0x10059e010>
      
    • 可以看到, 打印出的内存地址不同,我们把这种copy称为深拷贝 (deep copy), 也称为值拷贝

      • 我们修改 p2 不会对 p1 造成影响, 因为栈中p1 p2两个指针指向堆中两个不同的地址

      屏幕快照 2020-07-16 下午5.32.03

    • 深拷贝的原因,是我们在 copyWithZone 方法中,重新 alloc 了内存空间 , 那假如我们不重新申请内存空间呢?

    • 重写copyWithZone 方法如下 :

      • 没有重新申请内存空间
      1
      2
      3
      4
      
      - (id)copyWithZone:(nullable NSZone *)zone{
          Person *copyObj = [self initWithName:_name gender:_gender];
          return copyObj;
      }
      
      • main函数中代码不改动,输出结果如下:
      1
      2
      
      测试[60355:1053881] Rose is female, <memory address: 0x100612520>
      测试[60355:1053881] Rose is female, <memory address: 0x100612520>
      
      • 发现 对 p2的修改,影响了 p1, 并且 p1 和 p2的内存地址是一样的。
    • 这种copy,我们称为 浅拷贝(Shadow Copy), 也叫指针拷贝

    • 没有为 p2 申请新的堆空间, 而是将 p2指针执行 p1 指向的地址 。

    • p2的修改,修改了堆空间的值,对p1产生影响

参考文献

大话数据结构

苹果开发文档