Objective-C 浅拷贝与深拷贝:从踩坑到彻底搞懂

0 阅读4分钟

引言

"为什么改了外面的字符串,我模型里的值也跟着变了?"

"不是说 copy 就是深拷贝吗,怎么元素地址还是一样的?"

如果你也被这些问题困扰过,恭喜你,来对地方了。今天我们从代码验证出发,一次性把 Objective-C 的浅拷贝和深拷贝彻底讲清楚。

一、strong 修饰 NSString 的经典坑

先看一个生产环境里最常见的 bug:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;  // ❌ 错误的写法
@end

// 使用场景
NSMutableString *mutableName = [NSMutableString stringWithString:@"张三"];
self.person.name = mutableName;

[mutableName appendString:@"(已修改)"];

NSLog(@"%@", self.person.name);  // "张三(已修改)" — 内部值被污染了!

为什么?

strong 修饰后的内存模型:

mutableName  ──→ [NSMutableString "张三"]  ←── person.name
               (同一个对象,同一块内存)

外部 appendString → 内部也跟着变了

解决方案

@property (nonatomic, copy) NSString *name;  // ✅ 标准写法
copy 修饰后的内存模型:

mutableName  ──→ [NSMutableString "张三"]
person.name  ──→ [NSString "张三"]          ← 独立不可变副本

外部修改 → 内部不受影响

这就是 copystrong 的本质区别:strong 共享对象,copy 创建独立副本。


二、copy vs mutableCopy:容器层面的行为

很多人以为这两个方法差不多,实际上它们的区别非常明确:

代码验证

// ========== 不可变源 ==========
NSArray *immutable = @[@"A", @"B", @"C"];

NSArray *copyResult = [immutable copy];           // 地址相同!Foundation 优化
NSMutableArray *mutableCopyResult = [immutable mutableCopy];  // 地址不同

// ========== 可变源 ==========
NSMutableArray *mutable = [NSMutableArray arrayWithObjects:@"A", @"B", nil];

NSArray *copyResult2 = [mutable copy];            // 地址不同!创建新不可变容器
NSMutableArray *mutableCopyResult2 = [mutable mutableCopy]; // 地址不同

��为矩阵

源对象copymutableCopy
不可变 (NSArray)返回自身(retain),地址相同NSMutableArray,地址不同
可变 (NSMutableArray)NSArray,地址不同NSMutableArray,地址不同

核心结论:

  • copy 始终返回不可变对象
  • mutableCopy 始终返回可变对象
  • 对不可变源 copy 是优化过的,直接 retain,不创建新对象

三、浅拷贝 vs 深拷贝:元素层面才是关键

容器地址不同 ≠ 深拷贝。浅拷贝和深拷贝的核心区别在于元素是否被拷贝。

浅拷贝验证

NSMutableArray *original = [NSMutableArray arrayWithObjects:@"A", @"B", @"C", nil];
NSArray *shallowCopy = [original copy];

NSLog(@"容器: %p vs %p", original, shallowCopy);   // 不同 ✅
NSLog(@"元素: %p vs %p", original[0], shallowCopy[0]);  // 相同  ← 浅拷贝!

内存模型:

original      ──→ [容器A] ──→ "A" ←── [容器B] ←── shallowCopy
                              → "B" ←─── ↑ 共享
                              → "C" ←─── ↑ 共享

结论:copymutableCopy 对容器元素都是浅拷贝,元素指针共享。


四、真正让元素地址不同的方法

三种常见的"深拷贝"写法:

NSMutableString *a = [NSMutableString stringWithString:@"A"];
NSMutableString *b = [NSMutableString stringWithString:@"B"];
NSMutableString *c = [NSMutableString stringWithString:@"C"];
NSArray *original = @[a, b, c];

// 方法1:归档解档
NSArray *deepCopy = [NSKeyedUnarchiver unarchiveObjectWithData:
                     [NSKeyedArchiver archivedDataWithRootObject:original]];

// 方法2:copyItems:YES
NSArray *deepCopy1 = [[NSArray alloc] initWithArray:original copyItems:YES];

// 方法3:手动递归
NSMutableArray *deepCopy2 = [NSMutableArray array];
for (id obj in original) {
    [deepCopy2 addObject:[obj copy]];
}

为什么之前验证时元素地址相同?

因为之前用的是 @"A"(不可变 NSString),它的 copy 就是返回自身:

NSString *str = @"A";
NSString *copied = [str copy];
NSLog(@"%p vs %p", str, copied);  // 相同!不可变对象 copy = retain

改成可变元素后,地址果然不同:

NSLog(@"元素: %p VS %p VS %p VS %p",
      original[0], deepCopy[0], deepCopy1[0], deepCopy2[0]);
// 元素: 0x600002a00010 VS 0x600002a00030 VS 0x600002a00050 VS 0x600002a00070
// 全部不同!✅

// 验证数据隔离
[a appendString:@"(被篡改)"];
NSLog(@"%@", original[0]);   // "A(被篡改)"  不受影响
NSLog(@"%@", deepCopy[0]);   // "A"             不受影响  ✅

五、正确的"深拷贝"心智模型

讨论深拷贝时,需要准确定义"深度":

层次含义示例
一层深拷贝对第一层可变元素调用 copy,创建不可变副本copyItems:YES
完全深拷贝递归处理所有嵌套层级归档解档

核心认知:

"深"不是要求所有元素都必须新建,而是对可变元素创建独立副本,防止外部修改影响内部。不可变元素本身不会被意外修改,copy 返回自身既安全又高效。


六、三种深拷贝方式的对比

方式深度要求性能适用场景
copyItems:YES一层元素实现 NSCopying较快一层嵌套足够
手动递归可控自己控制深度灵活需要自定义逻辑
归档解档完全所有元素实现 NSCoding最慢需要完全独立副本

七、总结

概念一句话记忆
strong vs copy需要保护内部数据的属性用 copy(NSString/NSArray等)
copy vs mutableCopycopy 返回不可变,mutableCopy 返回可变
浅拷贝新容器 + 旧元素(指针共享)
深拷贝新容器 + 对可变元素创建不可变副本