设计模式之原型模式(深入理解OC中的NSCopying协议以及浅拷贝、深拷贝)
- 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式其实就是从一个对象再创建另一个可定制的对象,而且不需知道任何创建的细节。
比如说,有一个Person类,有firstName、lastName、friends这三个属性,代码如下:
#import <Foundation/Foundation.h> @interface ZYPerson : NSObject { NSMutableSet *_friends; } @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName; - (void)addFriend:(ZYPerson *)person; - (void)removeFriend:(ZYPerson *)person; @end #import "ZYPerson.h" @implementation ZYPerson - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName { if (self = [super init]) { _firstName = firstName; _lastName = lastName; _friends = [[NSMutableSet alloc] init]; } return self; } - (void)addFriend:(ZYPerson *)person { [_friends addObject:person]; } - (void)removeFriend:(ZYPerson *)person { [_friends removeObject:person]; } @end
viewController里面的代码:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. ZYPerson *personOne = [[ZYPerson alloc] initWithFirstName:@"张" lastName:@"三"]; ZYPerson *personTwo = [[ZYPerson alloc] initWithFirstName:@"李" lastName:@"四"]; [personOne addFriend:personTwo]; }
现在有这样的一个需求,有一个人,也叫张三,也只有李四一个好友,如果不用原型模式,就会使下面的代码:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. ZYPerson *personOne = [[ZYPerson alloc] initWithFirstName:@"张" lastName:@"三"]; ZYPerson *personTwo = [[ZYPerson alloc] initWithFirstName:@"李" lastName:@"四"]; [personOne addFriend:personTwo]; ZYPerson *personThree = [[ZYPerson alloc] initWithFirstName:personOne.firstName lastName:personOne.lastName]; [personThree addFriend:personTwo]; }
这样,Person类只有两三个属性还好说,只是简单的写下,如果Person类有十几个属性,有上百个朋友,这代码量是很大的,而且这种代码也是没有必要的,谁上谁都会写,至少我是不愿意写这种垃圾代码的。
如此,原型模式就可以比较好的解决这样一个问题,在iOS开发中,原型模式依赖于NSCopying协议,需要实现-copyWithZone方法,Person类代码如下:
#import <Foundation/Foundation.h> @interface ZYPerson : NSObject <NSCopying> { NSMutableSet *_friends; } @property (nonatomic, copy, readonly) NSString *firstName; @property (nonatomic, copy, readonly) NSString *lastName; - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName; - (void)addFriend:(ZYPerson *)person; - (void)removeFriend:(ZYPerson *)person; @end #import "ZYPerson.h" @implementation ZYPerson - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName { if (self = [super init]) { _firstName = firstName; _lastName = lastName; _friends = [[NSMutableSet alloc] init]; } return self; } - (void)addFriend:(ZYPerson *)person { [_friends addObject:person]; } - (void)removeFriend:(ZYPerson *)person { [_friends removeObject:person]; } - (id)copyWithZone:(NSZone *)zone { ZYPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName lastName:_lastName]; copy->_friends = [_friends mutableCopy]; return copy; } @end
viewController代码:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. ZYPerson *personOne = [[ZYPerson alloc] initWithFirstName:@"张" lastName:@"三"]; ZYPerson *personTwo = [[ZYPerson alloc] initWithFirstName:@"李" lastName:@"四"]; [personOne addFriend:personTwo]; ZYPerson *personThree = [personOne copy]; NSLog(@"%@ %@",personThree.firstName, personThree.lastName); }
可以看到,在初始化条件不发生改变的情况下,copy是最好的办法,既隐藏了对象创建的细节,对性能也是有着显著提高的,最主要的一点,就是没必要去重复写垃圾代码,浪费大量时间。
2. 浅拷贝(copy)与深拷贝(mutableCopy)
copy与mutableCopy的区别,如果学过c或者c++的朋友会知道指针这样一个概念,就是在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存
而深拷贝就是不仅增加了一个指针,并且还申请了一个新的内存,是这个新增加的指针指向这个新的内存,采用深拷贝的情况下,释放内存的时候就不会出现在浅拷贝时重复释放同一内存的错误。
上面的这句代码:
copy->_friends = [_friends mutableCopy];
使用了->语法,因为_friend并非属性,而是在内部使用的实例变量。这里有一个问题,为什么要拷贝_friend实例变量呢?不拷贝这个变量,直接让两个对象共享同一个可变的set是否更简单?
如果这么做了,那么再给personOne添加一个新的朋友之后,拷贝过来的那个对象,也就是personThree也会“神奇”的与之为朋友了。在我上面写的那个实例中,这不是我想要的效果。然而,如果那个set是不可变的,那么就无需复制,因为其中的内容不可能会改变,所以就不用担心会出现上面的问题。如果复制了,那么内存中会有两个一模一样的set,造成了浪费。
引申出这样一个问题(面试题):怎样使用copy关键字?
以前我是这么回答的:
一般使用retain或者strong修饰属性时,是使引用对象的指针指向同一个对象,即为同一块内存地址。只要其中有一个指针变量被修改时所有其他引用该对象的变量都会被改变。
而使用copy关键字修饰在赋值时是释放旧对象,拷贝新对象内容。重新分配了内存地址。以后该指针变量被修改时就不会影响旧对象的内容了。
copy只有实现NSCopying协议的对象类型才有效。
常用于NSString和Block。
有一定错误,应该这样修正:
一般使用retain或者strong修饰属性时,是使引用对象的指针指向同一个对象,即为同一块内存地址。只要其中有一个指针变量被修改时所有其他引用该对象的变量都会被改变。
而copy关键字修饰时,如果新的对象是不可变的,那么它是直接引用新对象的内存地址,并不重新分配内存地址,如果新对象是可变的,那么在赋值时是释放旧对象,拷贝新对象内容。重新分配了内存地址。以后该指针变量被修改时就不会影响旧对象的内容了。
copy只有实现NSCopying协议的对象类型才有效。
常用于NSString和Block。