copy和mutableCopy方法
copy方法用于复制对象的副本,该方法总是返回对象的不可修改的副本,即使被复制的对象本身是可修改的。例如调用NSMutableString的copy方法将会返回不可修改的字符串对象。
mutableCopy方法也用于复制对象的副本,不同的是,该方法总是返回该对象的可修改副本,即使被复制的对象本身是不可修改的。例如调用mutableCopy方法复制NSString的,返回的是一个NSMutableString对象。
这两个方法的共同点是:都是用于复制对象的副本;都返回的是原对象的副本;对复制的副本进行修改时,原对象通常不受影响。
下面对这两个方法的使用给出具体的代码示例:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
@autoreleasepool {
NSMutableString* game = [NSMutableString stringWithString:@"原神启动!"];
//复制game字符串的可变副本
NSMutableString* gameCopy = [game mutableCopy];
//修改副本,对原字符串没有任何影响
[gameCopy replaceCharactersInRange:NSMakeRange(0, 2) withString:@"崩铁"];
//此处可以看到原字符串的值并没有改变
NSLog(@"game的值为 %@", game);
//此处可以看到字符串副本发生了改变
NSLog(@"gameCopy的值为 %@", gameCopy);
NSString* str = @"艾尔海森";
//复制str(不可变字符串)的可变副本
NSMutableString* strCopy = [str mutableCopy];
//向可变字符串后面追加字符串
[strCopy appendString:@"是教令院的书记官。"];
NSLog(@"strCopy的值为 %@", strCopy);
//调用game(可变字符串)的copy方法,程序返回一个不可修改的副本
NSMutableString* gameCopy2 = [game copy];
//如果此时试图对gameCopy2进行修改操作(例如修改、添加等),编译时不会报错,但运行后代码就会出错
//[gameCopy2 appendString:@"原神是一款探索冒险游戏"];
//(运行后代码就会出错 笔者在这里直接就注释掉了)
}
return 0;
}
运行结果:
NSCopying与NSMutableCopy协议
上面我们说了可以调用copy方法和mutableCopy方法来复制对象的副本,那么现在思考一下:我们自定义的类可以调用copy方法和mutableCopy方法来复制对象副本吗?
答案是不可以的!因为当程序调用对象的copy(mutableCopy)方法复制自身时,程序底层需要调用copyWithZone:(mutableCopyWithZone:)方法来完成实际的复制工作,copy(mutableCopy)方法返回的实际上就是copyWithZone:(mutableCopyWithZone:)方法的返回值。而我们的自定义类中并没有事先对copyWithZone:(mutableCopyWithZone:)进行实现,所以在调用时会出现找不到copyWithZone:(mutableCopyWithZone:)的错误提示。
为了保证一个对象可调用 copy 方法来复制自身的不可变副本,通常需要做如下事情:
- 让该类实现 NSCopying 协议。
- 让该类实现 copyWithZone:方法。
同理,为了保证一个对象可以调用 mutableCopy 方法来复制自身的可变副本,通常需
要做如下事情:
- 让该类实现 NSMutableCopying 协议。
- 让该类实现 mutableCopyWithZone:方法。
由此,我们如果要在自定义类里调用copy方法和mutableCopy方法来复制对象副本,就需要在自定义类的接口部分声明NSCopying(NSMutableCopying)协议,然后在自定义类的实现部分增加copyWithZone:(mutableCopyWithZone:)方法。
下面给出在自定义类里调用copy方法和mutableCopy方法来复制对象副本的代码示例:
接口部分:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKGame:NSObject<NSCopying>
@property(nonatomic, strong)NSMutableString* name;
@property(nonatomic, assign)int age;
@end
NS_ASSUME_NONNULL_END
实现部分:
#import "FKGame.h"
@implementation FKGame
@synthesize name;
@synthesize age;
//在FKGame的实现部分增加copyWithZone:方法
- (id)copyWithZone:(NSZone *)zone {
NSLog(@"--执行copyWithZon--");
//使用zone参数创建FKGame对象
FKGame* game = [[[self class]allocWithZone:zone]init];
game.name = self.name;
game.age = self.age;
return game;
}
@end
主函数:
#import <Foundation/Foundation.h>
#import "FKGame.h"
int main(int argc, const char* argv[])
{
@autoreleasepool {
//创建一个game对象
FKGame* game1 = [FKGame new];
game1.name = [NSMutableString stringWithString:@"原神"];
game1.age = 4;
//复制副本(不添加对应协议这里会报错)
FKGame* game2 = [game1 copy];
game2.name = [NSMutableString stringWithString:@"崩铁"];
game2.age = 3;
NSLog(@"game1的名字是:%@,已经发布了 %d 年了", game1.name, game1.age);
NSLog(@"game2的名字是:%@,已经发布了 %d 年了", game2.name, game2.age);
}
return 0;
}
运行结果:
浅复制与深复制
浅复制
当对象的实例变量是指针变量时,如果程序只是复制该指针的地址,而不是真正复制指针所指的对象,这种复制方法就被称为浅复制。
对于浅复制而言,程序是在内存中复制了两个对象,这两个对象的指针变量将会指向同一个变量,即两个对象依然存在共用的部分。
下面会用代码进行演示:(该部分代码的接口部分和实现部分与刚才NSCopying与NSMutableCopy协议的大致相同,在这里就不再给出)
主函数:
#import <Foundation/Foundation.h>
#import "FKGame.h"
int main(int argc, const char* argv[])
{
@autoreleasepool {
//创建一个game1对象
FKGame* game1 = [FKGame new];
game1.name = [NSMutableString stringWithString:@"原神"];
game1.age = 4;
//复制副本(不添加对应协议这里会报错)
FKGame* game2 = [game1 copy];
//修改game2的name属性
[game2.name replaceCharactersInRange:NSMakeRange(0, 2) withString:@"崩铁"];
//查看game1、game2的name属性
NSLog(@"game1的name是:%@", game1.name);
NSLog(@"game2的name是:%@", game2.name);
}
return 0;
}
运行结果:
从上述代码,我们可以看出来,程序调用了game1的copy方法复制了一个副本,并将该副本赋给了game2变量,然后程序对game2的name属性进行了修改,输出game1和game2的name属性,发现两者都被修改了。这是因为name本身只是一个指针变量,里面存放的是字符串的地址,并不是字符串本身,这样赋值的效果是让game1和game2的name属性指向同一个字符串,这样无论程序修改哪个name实例变量值,另一个也会随着改变。
深复制
深复制与浅复制相反,它不仅会复制对象本身,而且会“递归”复制每个指针类型的实例变量,直到两个对象没有任何共用的部分。
要想实现深复制,只需将实现部分的copyWithZone:改为如下形式即可。
- (id)copyWithZone:(NSZone *)zone {
NSLog(@"--执行copyWithZon--");
//使用zone参数创建FKGame对象
FKGame* game = [[[self class]allocWithZone:zone]init];
game.name = [self.name mutableCopy];
game.age = self.age;
return game;
}
代码的接口部分和其余实现部分与刚才NSCopying与NSMutableCopy协议的大致相同,主函数部分与浅复制的大致相同,这里就不给出了。
运行结果:
由上述copyWithZone:程序和运行结果可以看出来,深复制是先将原对象的name实例变量复制了一份可变副本,再将可变副本的值赋给新对象的name实例变量。这样就保证了两个FKGame对象之间没有任何共用的部分,成功实现了深复制。
一般来说,Foundation框架中的类大部分都只实现了浅复制。
setter方法的复制选项
前面说到在合成setter和getter方法的时候可以使用copy指示符,该指示符就是指定当程序调用setter方法复制的时候,实际上是将传入参数的副本赋给程序的实例变量。
下面进行代码示例:
接口部分:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKItem:NSObject
@property (nonatomic, copy) NSMutableString* name;
@end
NS_ASSUME_NONNULL_END
实现部分:
#import "FKItem.h"
@implementation FKItem
@synthesize name;
@end
主函数:
#import <Foundation/Foundation.h>
#import "FKItem.h"
int main(int argc, const char* argv[])
{
@autoreleasepool {
FKItem* item = [FKItem new];
item.name = [NSMutableString stringWithString:@"疯狂iOS讲义"];
[item.name appendString: @"fkit"];
}
return 0;
}
运行结果:
运行后,我们会发现程序出现以上报错,意为,代码尝试使用 appendString: 方法来修改一个 NSString 对象,而该对象是一个不可变的对象。出现这种错误的原因是,我们在定义name属性时使用了copy指示符,该指示符指定调用setName:方法时(通过点语法赋值时,实际上是调用对应的setter方法)程序实际上会使用参数的副本对name实例变量复制。
这里的setter方法相当于在实现部分有如下代码:
- (void)setName:(NSMutableString *) aname
{
name = [aname copy];
}
copy方法默认复制该对象的不可变副本,虽然程序传入的NSMutableString,但程序调用该参数的copy方法得到的是不可变副本。所以,程序赋给FKItem对象的name实例变量的值依然是不可变字符串。
定义合成 getter、setter 方法时并没有提供 mutableCopy 指示符。因此,即使定义实例变量时使用了可变类型,但只要使用 copy 指示符,实例变量实际得到的值总是不可变对象。