一、对象的可变性
OC的类有可变的类和不可变的类,这与变量和常量是不同的。可变类生成的对象是可变对象,不可变类生成的对象是不可变对象。
可变对象,不可变对象,变量,常量
可变对象:内容可以改变的对象。
不可变对象:内容不可以更改的对象。
变量:值可以更改。
常量:值不可以更改。
可变对象可以是常量也可以是变量。
不可变对象可以是常量,也可以是变量。
对象为什么要区分可变和不可变呢?
这是为了从实现的复杂度方面考虑的。例如,要允许删除或者增加字符串的某个字母,或者给数组追加成百上千的元素,这就要在一开始设计的时候做好对象的数据结构和方法。如果一开始就确定这个对象是不可变的,那么就不需要为他设计那么复杂的数据结构了,实现起来也简单的多。
例如:给路径"/Users/wenhuanhuan/Desktop/a.txt"更改文件后缀名为”a.bak”
实现方法当然是各种各样,各有千秋,这里就举出三种实现方法,其中有用到可变与不可变的对象哟。
//方法一
NSString * oldFile2 = @"/Users/wenhuanhuan/Desktop/a.txt”;
NSString * newP = [oldFile2 stringByReplacingOccurrencesOfString:@"a.txt" withString:@"a.bak”];
NSLog(@"方法一:新的路径: %@", newP);
//方法二
NSString * path = @"/Users/wenhuanhuan/Desktop/a.txt”;
//获取文件名
NSString * fileName = [path lastPathComponent];
//删除文件的扩展名
fileName = [fileName stringByDeletingPathExtension];
//给文件加上新的扩展名.bak
fileName = [fileName stringByAppendingPathExtension:@"bak”];
//重新生成新的路径
NSString * tempPath = [path stringByDeletingLastPathComponent];
tempPath = [tempPath stringByAppendingPathComponent:fileName];
NSLog(@"方法二:新的路径是: %@", tempPath);
//方法三
NSString * oldFile = @"/Users/wenhuanhuan/Desktop/a.txt”;
//分解路径为数组
NSMutableArray *olds = [NSMutableArray arrayWithArray:[oldFile componentsSeparatedByString:@"/“]];
//取最后一个元素也就是a.txt
NSString *lastStr = [olds lastObject];
//再次分解a.txt为数组,取后缀
NSMutableArray *oldFileName = [NSMutableArray arrayWithArray:[lastStr componentsSeparatedByString:@".”]];
//更改后缀
[oldFileName replaceObjectAtIndex:oldFileName.count - 1 withObject:@"bak”];
//重新拼接
[oldFileName insertObject:@"." atIndex:1];
NSMutableString * tempFileName = [oldFileName componentsJoinedByString:@","].mutableCopy;
tempFileName = [tempFileName stringByReplacingOccurrencesOfString:@"," withString:@""].mutableCopy;
[olds removeLastObject];
[olds addObject:tempFileName];
NSString * newPath = [olds componentsJoinedByString:@"/“];
NSLog(@"方法三:新路径: %@", newPath);
运行结果:
说起OC对象的可变和不可变,就不得不提到一个方法,就是:
-(id)mutableCopy;
这是声明在NSObject中的方法。
此方法能够将不可变对象变成可变对象,例如,上面例子中的:
NSMutableString * tempFileName = [oldFileName componentsJoinedByString:@","].mutableCopy;
[oldFileName componentsJoinedByString:@",”] 这个方法返回的是一个NSString对象,是一个不可变对象,但是我们需要一个可变的字符串,这个时候就可使用mutableCopy方法,让他变成一个可变的字符串,最后我们就可以更改这个可变的字符串了。
那么我们再来深入想一想,我们使用这个方法生成的对象和原来的对象是不是一个地址呢?有何关系呢?
NSString * str1 = @“hello”;
NSMutableString * str2 = str1.mutableCopy;
NSLog(@"str1: %p, str2: %p", str1, str2);
看看结果:
地址是不一样的,也就是这是两个对象。
在NSObject中还有一个方法,叫做copy,在我们的截图中也可以看到,就在mutableCopy的上一行,我们也来试一试。
看看结果:
str1和str3的地址是一样的,也就是使用的同一块地址空间,只是引用计数增加了,这个叫做『浅拷贝』。
这里,我们再来看看几个概念,『单层深拷贝』,『浅拷贝』,『完全拷贝』。
二、深浅拷贝以及完全深拷贝
浅拷贝:就是像上面的例子一样,只是复制了指针,对象的地址并不改变,只是给对象增加了一个引用计数。
单层深拷贝:这里的单层深复制是针对集合类来说的,指的是只复制外层的集合类对象,里面的元素并没有复制。
完全拷贝:也是针对集合类来说的,完全拷贝不仅复制了对象自身,生成了新的对象,集合中的元素对象也被复制了一份新的。
我们用代码来具体的看一看到底什么是深浅拷贝呢。
- 浅拷贝
1.对不可变对象发送copy,无论对象的接收者是可变的还是不可变的,他们的地址不变,是浅拷贝。
注意:无论对象接收者的类型是可变的还是不可变的,使用copy复制出来的对象都是不可变的。因为copy方法返回类型是id,使用可变指针接收也不会报错,但是,一旦使用可变对象的方法去改变它的时候,就会造成崩溃。
NSString * str1 = @“hello”;
NSString * str2 = str1.copy;
//这是一种不安全的写法,因为mstr2实质上是不可变的,
//使用mstr2更改其中的元素的时候,会崩溃。
NSMutableString * mstr2 = str1.copy;
NSLog(@"str1: %p, str2: %p, mstr2: %p", str1, str2, mstr2);
//使用copy出来的对象都是不可变对象,即使使用可变指针来接收,对象本身依然是不可变的。
// [mstr2 appendString:@"小苗儿"];//崩溃
// NSLog(@"%@", mstr2);
打印结果:
从结果可以看出来,str1,str2,mstr的地址都是一样的,是浅拷贝。
我们更改一下str1的值看一看,结果会如何呢?str2,str3,mstr2,mstr3的值又会不会变化呢?
NSLog(@"更改str的值”);
str1 = @"你好!”;
NSLog(@"str1: %p, str2: %p, mstr2: %p", str1, str2, mstr2);
NSLog(@"str1: %@", str1);
NSLog(@"str2: %@", str2);
NSLog(@"mstr2: %@", mstr2);
NSLog(@"str3: %@", str3);
NSLog(@"mstr3: %@", mstr3);
打印结果
从打印结果可以看出,str1的值更改之后,它的地址改变了,而str2等的地址没有改变,只有str1的值变成了”你好!”,其他值没有更改。
我们用示意图来理解,一目了然。
我们再来看看直接赋值,一个值更改了,另一个会不会改变。
NSString * s1 = @"明天是个好天气”;
NSString * s2 = s1;
NSLog(@"更改前, s1: %p, s2: %p", s1, s2);
s2 = @"下雨啦”;
NSLog(@"更改后,s1: %p, s2: %p", s1, s2);
NSLog(@"s1: %@, s2: %@", s1, s2);
打印结果:
跟浅拷贝一样,值更改后,地址就改变了,对s1没有影响。
我们再试试别的类型,例如NSNumber。
//再来看看NSNumber类型,不能使用mutableCopy哟
NSNumber * num1 = [NSNumber numberWithInt:20];
NSNumber * num2 = num1.copy;
NSNumber * num3 = num1.copy;
NSLog(@"num1: %p", num1);
NSLog(@"num2: %p", num2);
NSLog(@"num3: %p", num3);
num1 = @200;
NSLog(@"num1: %@, num2: %@, num3: %@", num1, num2, num3);
打印结果:
跟NSString一样,更改前,地址是一样的,是浅拷贝,更改后,被更改的对象的地址改变了,其他的地址不变,值也没有影响。
再试试NSArray。
NSArray * array = @[@"1", @"2", @"3”];
NSArray * array2 = array.copy;//浅拷贝
NSMutableArray * array3 = array.mutableCopy; //深拷贝
NSArray * array4 = array;//直接赋值
NSLog(@"array: %p, array2: %p, array3: %p, array4: %p", array, array2, array3, array4);
printArray(array);
printArray(array2);
printArray(array3);
printArray(array4);
NSLog(@"更改array的值”);
array = @[@"5", @"6", @"7", @"8”];
NSLog(@"array: %p, array2: %p, array3: %p, array4: %p", array, array2, array3, array4);
printArray(array);
printArray(array2);
printArray(array3);
printArray(array4);
printArray方法
void printArray(NSArray * array) {
printf("\n\n”);
for (NSString * obj in array) {
NSLog(@"%p, %@", obj, obj);
}
}
备注:我新建的是控制台程序,方法声明在main函数中,所以方法的声明跟C语言一样。
在main函数的最上面,加上方法声明。
继续上面那个测试,看看数组的值更改前后,地址是否变化呢。
打印结果
值更改前,除了深拷贝的array3的地址不一样之外,其他地址都是一样的。再看看值更改之后。
被更改的array的地址发生了变化,与上面的NSString,NSNumber的情况都一样。
总结:
对不可变对象发送copy消息,是浅拷贝,地址不变;
更改浅拷贝出来的对象的值,被更改的对象地址发生改变,其他浅拷贝出来的对象值不受影响,原对象也不受影响。
copy出来的对象都是不可变对象。
2.对不可变对象发送mutableCopy,地址改变,是深拷贝
NSString * str3 = str1.mutableCopy;
NSMutableString * mstr3 = str1.mutableCopy;
NSLog(@"str1: %p, str3: %p, mstr3: %p", str1, str3, mstr3);
打印结果
结果发现,三个地址都是不一样的,是不同的对象,也就是深拷贝了。
3.对可变对象发送copy消息,地址改变,是深拷贝
NSMutableString * ms1 = [NSMutableString stringWithString:@"world”];
NSString * ims1 = ms1.copy;
NSMutableString * ms1_1 = ms1.mutableCopy;
4.对可变对象发送mutableCopy,地址改变,是深拷贝
NSMutableString * ms2 = ms1.mutableCopy;
NSLog(@"ms1: %p, ims1: %p, ms1_1: %p, ms2:%p",ms1,ims1,ms1_1,ms2);
ms1 = [NSMutableString stringWithString:@"世界那么大”];
NSLog(@"ms1: %@", ms1);
NSLog(@"ims1: %@", ims1);
NSLog(@"ms1_1: %@", ms1_1);
NSLog(@"ms2: %@", ms2);
打印结果
不同地址的对象之间,值得更改肯定是互不影响的。
深拷贝示意图
总结:
1.对不可变对象发送copy消息,地址不变,是浅拷贝;
2.对不可变对象发送mutableCopy,地址改变,是深拷贝;
3.对可变对象发送copy消息,地址改变,是深拷贝;
4.对可变对象发送mutableCopy,地址改变,是深拷贝。
快速记忆:
只有不可变对象的copy是浅拷贝,其他都是深拷贝。
简化记忆:
不可变+copy,浅拷贝;其他深拷贝。
说到这里,似乎已经说完了深浅拷贝了,不就这四种情况吗?还有别的吗?有,深拷贝还能分出个单层深拷贝和完全深拷贝呢。
单层深拷贝和完全深拷贝
单层深拷贝和完全拷贝是针对集合类型说的,因为集合类型才会分层,才有嵌套。
单层深拷贝
只拷贝对象的一层或几层,不能把所有的元素都拷贝一份。完全深拷贝
无论集合有几层,所有的元素都是新的地址,新的对象。
我们就以数组为例,来看看吧。
NSMutableString * obj = [NSMutableString stringWithString:@"one”];
NSMutableArray * marray = [NSMutableArray arrayWithObjects:obj, @"two", @"three", nil];
NSMutableArray * marray1 = [marray mutableCopy];
NSArray * imarray = [marray copy];
printf("\n\n”);
NSLog(@"数组地址打印:”);
NSLog(@"marray: %p", marray);
NSLog(@"marray1: %p", marray1);
NSLog(@"imarray: %p", imarray);
NSLog(@"数组元素的地址打印:”);
printArray(marray);
printArray(marray1);
printArray(imarray);
NSLog(@"更改数组第一个元素的值”);
[obj appendString:@" add some datas”];
NSLog(@"更改后数组元素的地址打印:”);
printArray(marray);
printArray(marray1);
printArray(imarray);
NSLog(@"marray : %@", marray);
NSLog(@"marray1 : %@", marray1);
NSLog(@"imarray : %@", imarray);
打印结果
数组地址:
结果看出,对可变数组发送copy或者mutableCopy,都是深拷贝,新的对象地址与原数组是不一样的。
再看看数组的元素的地址:
我们发现,数组的元素的地址并没有改变,跟原来数组中的元素地址还是一样的,这就是单层深拷贝
了。
再看看更改了元素之后呢。
发现,第一个元素的地址没有改变,跟之前一样,其他数组的第一个元素的值也改变了。
我们用示意图来表示单层深拷贝,如下图:
汇总一个图
那么,如何让数组的元素也进行拷贝呢?我们先试试第一种方法。
方法一, 使用 - (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
NSMutableArray * newArray = [[NSMutableArray alloc] initWithArray:marray copyItems:YES];
[obj appendString:@" new item”];
printArray(marray);
printArray(newArray);
看看打印结果
第一个元素的地址改变了,并且,更改了第一个元素的值之后,newArray的值没有改变,似乎解决了问题,这是完全拷贝吗?其实,不然。
注意:
此方法使元素中均执行[xxx copy]方法。这也是在marray中放入NSMutableString的原因。如果放入的是NSArray或者NSString,执行copy后,只会发生指针复制, 像@“two”,@“three”,看看打印地址,跟marray中的地址还是一样的;如果放入的是未实现NSCopying协议的对象,如自定义对象,调用这个方法甚至会crash,因为自定义对象没有copy方法。
如果把marray放在一个可变的数组中,作为数组的一个元素,上面的方法再次失效,我们一起看看。
NSMutableString * red = [NSMutableString stringWithString:@"red”];
NSMutableString * yellow = [NSMutableString stringWithString:@"yellow”];
NSMutableArray * bigArray = [NSMutableArray arrayWithObjects:marray, red, yellow, nil];
NSMutableArray * bigArrayCopy = [[NSMutableArray alloc] initWithArray:bigArray copyItems:YES];
NSMutableString * tempStr = bigArray[0][0];
[tempStr insertString:@"11111" atIndex:0];
NSLog(@"bigArray: %@", bigArray);
NSLog(@"bigArrayCopy: %@", bigArrayCopy);
打印结果:
说明它们的元素共用了一块地址,一个值更改了,另一个也更改了。所以,使用上面的方法,并没有完全拷贝,
[[NSMutableArray alloc] initWithArray:bigArray copyItems:YES]
这个方法只能深复制一层,再多套一层就不行了。
那么到底怎样才能完全拷贝呢?我们可以使用归档和解档。
方法二, 使用归档和解档
//方法二,使用归档和解档
NSMutableArray * myArrayCopy = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:bigArray]];
NSMutableString * tempStr2 = bigArray[0][0];
[tempStr2 insertString:@"0000" atIndex:0];
NSLog(@"bigArray: %@", bigArray);
NSLog(@"myArrayCopy: %@", myArrayCopy);
看看结果:
看起来是好用的哟。
多嵌套几层试试,看看还是好用的吗?
NSMutableString * xiaoming = [NSMutableString stringWithString:@"xiaoming”];
NSMutableArray * names = [NSMutableArray arrayWithObjects:xiaoming, @"xiaohua", @"xiaoli", nil];
NSMutableArray * students = [NSMutableArray arrayWithObjects:names, @"OC", @"Java", nil];
NSMutableArray * people = [NSMutableArray arrayWithObjects:students,@"beijing", nil];
NSMutableArray * peopleCopy = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:people]];
NSMutableString * temp = people[0][0][0];
[temp appendString:@" 100 “];
NSLog(@"people: %@", people);
NSLog(@"peopleCopy: %@", peopleCopy);
打印结果:
依然是好用的,说明使用归档和解档实现了完全拷贝。
如果是我们自定义的类,可不可以copy呢?
自定义类的copy
我们新建一个类,起名儿叫做ClassA,如下图:
classA.h:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ClassA : NSObject
@property(nonatomic, copy)NSString * name;
@property(nonatomic, assign)int num;
@property(nonatomic, strong)NSArray * array;
-(instancetype)initWithName:(NSString *)name num:(int)num;
@end
NS_ASSUME_NONNULL_END
classA.m
#import “ClassA.h”
@implementation ClassA
-(instancetype)initWithName:(NSString *)name num:(int)num {
if (self = [super init]) {
self.name = name;
self.num = num;
self.array = @[];
}
return self;
}
@end
在main函数中试试
#import <Foundation/Foundation.h>
#import “ClassA.h”
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here…
NSLog(@"Hello, World!”);
ClassA * a = [[ClassA alloc] initWithName:@"小明" num:1];
ClassA * a2 = a.copy;
NSLog(@"a: %@", a);
NSLog(@"a: %@", a2);
}
return 0;
}
运行,崩溃,崩溃信息如下:
说ClassA没有实现copyWithZone方法。
原来,自定义类要想使用copy方法,就需要遵守NSCopying协议,实现copyWithZone方法,我们来试一试。
把classA修改如下:
再次运行程序,没有崩溃,看看结果
由于我们的copyWithZone中写的是浅复制,直接返回的是自身,所以,我们的两个对象是一样的,地址也是一样的。我们重写一下description方法,看看他们的属性是否一样。
在运行看看
属性也是相同的,属性的地址也是一样的,如下图:
如果我想在自定义类实现copy深复制,该如何实现呢?
单层深复制
//单层深复制
-(id)copyWithZone:(NSZone *)zone {
ClassA * a = [[ClassA allocWithZone:zone] init];
a.name = self.name;
a.num = self.num;
a.array = self.array;
return a;
}
测试:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here…
NSLog(@"Hello, World!”);
ClassA * a = [[ClassA alloc] initWithName:@"小明" num:1];
ClassA * a2 = a.copy;
NSLog(@"a: %p, %@", a, a);
NSLog(@"a2: %p, %@", a2, a2);
}
return 0;
}
结果:
地址不同哟,是两个对象。为什么说是单层深复制呢?我们来看看他们的属性地址是否一样呢?
我们修改一下description方法,让他打印出属性的地址来。
- (NSString *)description
{
return [NSString stringWithFormat:@"对象地址%p: name: %p, %@, num: %d, array: %p, %@",self, self.name, self.name, self.num, self.array, self.array];
}
测试:
ClassA * a = [[ClassA alloc] initWithName:@"小明" num:1];
NSMutableString * str = [NSMutableString stringWithString:@"1”];
a.array = @[str, @"2", @"3”];
ClassA * a2 = a.copy;
NSLog(@"a: %@", a);
NSLog(@"a2: %@", a2);
看看结果:
属性的地址是一样的。我们更改array的元素的值再看看。
ClassA * a = [[ClassA alloc] initWithName:@"小明" num:1];
NSMutableString * str = [NSMutableString stringWithString:@"1”];
a.array = @[str, @"2", @"3”];
ClassA * a2 = a.copy;
[str appendString:@" hello”];
NSLog(@"a: %@", a);
NSLog(@"a2: %@", a2);
看看打印
就是我们之前说的单层深拷贝。如果想要实现完全复制,还是需要用到上面的解档和归档。
说到copy,很多面试都会问到一个问题,什么时候属性使用copy进行修饰?
对于拥有可变和不可变的类,如NSString,NSArray,NSSet等,修饰它们的不可变属性的时候,使用copy进行修饰。为什么呢?因为当用可变属性给不可变属性赋值的时候,可变值的修改会影响属性的值。看例子比较清楚。
我们新建一个类ClassB
测试:
看看结果:
用copy修饰的数组没有改变,用strong修饰的数组多了一个元素。
注意:
但是对于可变对象类型,如NSMutableString、NSMutableArray等则不可以使用copy修饰,因为Foundation框架提供的这些类都实现了NSCopying协议,使用copy方法返回的都是不可变对象,如果使用copy修饰符在对可变对象赋值时则会获取一个不可变对象,接下来如果对这个对象进行可变对象的操作则会产生异常,因为OC没有提供mutableCopy修饰符,对于可变对象使用strong修饰符即可。
本节学习了对象的可变性和浅复制,深复制,完全复制。
时间总是碎片化的,断断续续的整理了几天才把这几个知识点测试完。现在才知道,大块的时间真的是奢侈啊。抓住时间,努力学习,让生活变得更温柔。
源码地址:
https://github.com/weiman152/iOSTestCode/tree/master/iOS/Lesson9_Foundation