集合对象可变与不可变的那点事

简介

在文章 NSString NSMutableString 可变与不可变的那些事儿 分享了关于 NSStringNSMutableStringcopy 以及 mutableCopy 之间的点滴。

今天跟大家分享一下集合类数据的可变与不可变性,再结合 copy 以及 mutableCopy 说一说注意事项。如果你仔细看过 NSString NSMutableString 可变与不可变的那些事儿 这篇文章,那么接下来看本篇会很轻松。

本篇内容主要涉及以下几个方面:

  • 在 OC 中的集合对象
  • 集合对象的 copy、mutableCopy
  • 可变与不可变集合对象之间等号赋值
  • property 中的集合对象的 copy 和 strong
  • 实际案例分析

为了说明问题,这里,我选用数组(NSArray)作为集合对象的代表,其他的集合类以此类推即可。

集合对象

Objective-C 中,非集合类对象指的是 NSStringNSNumberNSValue 之类的对象,除了 NSString 有对应的可变类 NSMutableString 外,NSNumberNSValue 都没有可变类与其对应。

集合类对象是指 NSArrayNSMutableArrayNSDictionaryNSMutableDictionaryNSSetNSMutableSet 之类的对象。

集合对象的 copy、mutableCopy

看一个具体例子,请接着看下面的示例代码和说明。

例子1:NSArray 的 copy、mutableCopy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSArray *array = [NSArray arrayWithObjects:@"veryitman.com", nil];
NSLog(@"array addr: %p, array: %@ ", array, array);
// 地址未变和array一致,内容也一致
NSArray *array1 = array;
NSLog(@"array1 addr: %p, array1: %@", array1, array1);
// 地址未变和array一致,内容也一致
// copy 之后仍然是不可变的数组对象
id array2 = [array copy];
NSLog(@"array2 addr: %p, array2: %@", array2, array2);
// 地址改变
id array3 = [array mutableCopy];
NSLog(@"array3 addr: %p, array3: %@", array3, array3);
// 进一步说明了经过mutableCopy后,array3变成了可变数组
[(NSMutableArray *)(array3) addObject:@"my blog"];
NSLog(@"array3 addr: %p, array3: %@", array3, array3);
// 因为array3地址变了,不会影响array的地址和值
NSLog(@"array addr: %p, array: %@ ", array, array);

小结 1:

1、不可变数组 copy 之后,仍然是不可变数组,其地址和内容不变,即拷贝了原对象的内容和指针,属于指针拷贝。

2、不可变数组 mutableCopy 之后,变成了可变数组,其地址发生了变化,即只拷贝了原对象的内容,指针没有拷贝,属于内容拷贝。

3、不可变数组之间的等号(=)赋值,是指针拷贝。

例子2:NSMutableArray 的 copy、mutableCopy

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
NSMutableArray *marray = [NSMutableArray arrayWithObjects:@"veryitman.com", nil];
NSLog(@"marray addr: %p, marray: %@ ", marray, marray);
// 地址未变和marray一致,内容也一致
NSMutableArray *marray1 = marray;
NSLog(@"marray1 addr: %p, marray1: %@ ", marray1, marray1);
// copy 之后,地址改变且变成了不可变的数组对象
id marray2 = [marray copy];
NSLog(@"marray2 addr: %p, marray2: %@ ", marray2, marray2);
// mutableCopy 之后,地址改变但仍是可变数组对象
id marray3 = [marray mutableCopy];
NSLog(@"marray3 addr: %p, marray3: %@ ", marray3, marray3);
// Crash:进一步说明了可变数组对象经过 copy 之后变成了不可变的marray2
// -[__NSSingleObjectArrayI addObject:]: unrecognized selector sent to instance 0x600002cbd320
// *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
// reason: '-[__NSSingleObjectArrayI addObject:]: unrecognized selector sent to instance 0x600002cbd320'
// [(NSMutableArray *)(marray2) addObject:@"my blog"];
// NSLog(@"marray2 addr: %p, marray2: %@ ", marray2, marray2);
// 进一步证明了mutableCopy 之后,marray3是可变数组
[(NSMutableArray *)(marray3) addObject:@"my blog"];
NSLog(@"marray3 addr: %p, marray3: %@ ", marray3, marray3);
// 因为marray3地址改变了,所以对marray3的操作不会影响原来的数组对象marray
// marray 地址和内容保持不变
NSLog(@"marray addr: %p, marray: %@ ", marray, marray);

小结 2:

1、可变数组 copy 之后,会变成不可变数组,其内容不变,但是地址改变了,即只拷贝了原对象的内容,没有进行指针拷贝,属于内容拷贝。

2、可变数组 mutableCopy 之后,仍然是不可变数组,其地址发生了变化,内容没有变化,即只拷贝了原对象的内容,指针没有拷贝,属于内容拷贝。

3、可变数组之间等号(=)赋值,是指针拷贝。

例子3:NSMutableArray 和 NSArray 之间等号赋值

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
/** 向不可变数组赋值可变数组 */
{
NSMutableArray *tDatas = [NSMutableArray arrayWithObjects:@"veryitman.com", nil];
NSLog(@"--1--- tDatas addr: %p, tDatas: %@", tDatas, tDatas);
// 类似但不同于可变数组的mutableCopy操作,此时 array 的地址未变和tDatas地址一致
// array的内容和地址未发生变化,和tDatas一致
NSArray *array = tDatas;
NSLog(@"--2--- array addr: %p, array: %@", array, array);
}
/** 向可变数组赋值不可变数组 */
{
NSArray *tDatas = [NSArray arrayWithObjects:@"veryitman.com", nil];
NSLog(@"--1--- tDatas addr: %p, tDatas: %@", tDatas, tDatas);
// 类似进行了不可变数组的 copy 操作
// array 仍旧是不可变的,地址和内容与tDatas一致
NSMutableArray *array = tDatas;
NSLog(@"--2--- array addr: %p, array: %@", array, array);
// crash: 还是不可变的数组
// -[__NSArrayI addObject:]: unrecognized selector sent to instance 0x6000025499c0
// Terminating app due to uncaught exception 'NSInvalidArgumentException',
// reason: '-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x6000025499c0'
// [array addObject:@"blog"];
}

输出结果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
--1--- tDatas addr: 0x6000023a8bd0, tDatas: (
"veryitman.com"
)
--2--- array addr: 0x6000023a8bd0, array: (
"veryitman.com"
)
--1--- tDatas addr: 0x600002dbbf00, tDatas: (
"veryitman.com"
)
--2--- array addr: 0x600002dbbf00, array: (
"veryitman.com"
)

以上是使用 NSArrayNSMutableArray 来进行测试的,NSDictionaryNSSet 以及其对应的可变类型都遵循上面总结的内容。

copy、strong 修饰属性

在属性中,我们如何来选择 copy 或者 strong 来作为集合数据的修饰语呢?

根据上面示例分析结果可以看出,在属性中,如果使用 strong 修饰不可变数组,那么在使用过程中(被可变数组赋值)该不可变数组有可能会变为可变数组。如果使用 copy 修饰可变数组,那么在使用过程中(被不可变数组赋值)该可变数组有可能变为不可变数组。

小结 3:

当修饰可变类型的属性时,如 NSMutableArrayNSMutableDictionaryNSMutableSet 等集合类型时,用 strong 修饰。

当修饰不可变类型的属性时,如 NSArrayNSDictionaryNSSet 等集合类型时,用 copy 修饰。

大家如果有兴趣可以参考文章 NSString NSMutableString 可变与不可变的那些事儿 的做法来验证上面的理论知识。

实际案例分析

再给大家举个实际的开发案例,我们需要定时上报目采集APP的数据,这个需求看起来是没有任何难度的。

我们使用代码来模拟一下上报数据的这个过程。

1
2
3
4
5
6
7
// 采集到的数据
NSMutableDictionary *tDatas = [NSMutableDictionary dictionaryWithCapacity:5];
[tDatas setObject:@"https://" forKey:@"req_m"];
NSLog(@"--采集数据--- tDatas addr: %p, tDatas: %@", tDatas, tDatas);
// 开始发送
[self sendDatas:tDatas];
NSLog(@"--上报完成,原数据--- tDatas addr: %p, tDatas: %@", tDatas, tDatas);

发送数据的模拟示例如下:

1
2
3
4
5
6
7
8
9
10
11
- (void)sendDatas:(NSDictionary *)datas
{
NSLog(@"--上报中--- datas addr: %p, datas: %@", datas, datas);
/** 下面两行代码只是为了模拟原数据被外界在传输过程中被改变,比如其他采集线程改变了它 */
if ([datas isKindOfClass:[NSMutableDictionary class]]) {
[(NSMutableDictionary *)datas setObject:@"veryitman.com" forKey:@"test_m"];
}
NSLog(@"--上报完成--- datas addr: %p, datas: %@", datas, datas);
}

根据上面例子3提到的,不可变向可变等号赋值时,原不可变对象会变成可变对象。

控制台输出日志,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--采集数据--- tDatas addr: 0x600000b788e0, tDatas: {
req_m = https://;
}
--上报中--- datas addr: 0x600000b788e0, datas: {
req_m = https://;
}
--上报完成--- datas addr: 0x600000b788e0, datas: {
req_m = https://;
test_m = veryitman.com;
}
--上报完成,原数据--- tDatas addr: 0x600000b788e0, tDatas: {
req_m = https://;
test_m = veryitman.com;
}

下面代码的代码,我是为了模拟原数据被其他代码改变了的情况,只是为了说明,不可变对象容易被外界影响和改变。

1
2
3
4
/** 下面两行代码只是为了模拟原数据被外界在传输过程中被改变,比如其他采集线程改变了它 */
if ([datas isKindOfClass:[NSMutableDictionary class]]) {
[(NSMutableDictionary *)datas setObject:@"veryitman.com" forKey:@"test_m"];
}

上面的总结又提到无论是可变对象还是不可变对象经过 copy 之后都是不可变对象的原理,我们修改一下代码,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)sendDatas:(NSDictionary *)datas
{
NSDictionary *copy_datas = [datas copy];
NSLog(@"--上报中--- copy_datas addr: %p, copy_datas: %@", copy_datas, copy_datas);
if ([copy_datas isKindOfClass:[NSMutableDictionary class]]) {
[(NSMutableDictionary *)copy_datas setObject:@"veryitman.com" forKey:@"test_m"];
} else {
NSLog(@"Yes, copy_datas 是不可变字典。");
}
NSLog(@"--上报完成--- copy_datas addr: %p, copy_datas: %@", copy_datas, copy_datas);
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--采集数据--- tDatas addr: 0x600003b08740, tDatas: {
req_m = https://;
}
--上报中--- copy_datas addr: 0x600003b08700, copy_datas: {
req_m = https://;
}
Yes, copy_datas 是不可变字典。
--上报完成--- copy_datas addr: 0x600003b08700, copy_datas: {
req_m = https://;
}
--上报完成,原数据--- tDatas addr: 0x600003b08740, tDatas: {
req_m = https://;
}

扫码关注,期待与你的交流~

坚持原创技术分享!