NSArray的深拷贝

122

有没有内置函数可以让我深度复制一个 NSMutableArray

我查了一下,有些人说 [aMutableArray copyWithZone:nil] 可以作为深拷贝。但我试过了,它似乎是浅拷贝。

现在我正在使用 for 循环手动复制:

//deep copy a 9*9 mutable array to a passed-in reference array

-deepMuCopy : (NSMutableArray*) array 
    toNewArray : (NSMutableArray*) arrayNew {

    [arrayNew removeAllObjects];//ensure it's clean

    for (int y = 0; y<9; y++) {
        [arrayNew addObject:[NSMutableArray new]];
        for (int x = 0; x<9; x++) {
            [[arrayNew objectAtIndex:y] addObject:[NSMutableArray new]];

            NSMutableArray *aDomain = [[array objectAtIndex:y] objectAtIndex:x];
            for (int i = 0; i<[aDomain count]; i++) {

                //copy object by object
                NSNumber* n = [NSNumber numberWithInt:[[aDomain objectAtIndex:i] intValue]];
                [[[arrayNew objectAtIndex:y] objectAtIndex:x] addObject:n];
            }
        }
    }
}

但我希望有一个更加简洁、简单的解决方案。


44
@Genericrich,深拷贝和浅拷贝是软件开发中非常明确的术语。Google.com可以提供帮助。 - Andrew Grant
1
也许有些混淆是因为在 Mac OS X 10.4 和 10.5 之间,不可变集合上的 -copy 行为发生了变化:http://developer.apple.com/library/mac/releasenotes/Cocoa/FoundationOlder.html#NSFileManager(向下滚动到“Immutable collections and copy behavior”)。 - user102008
1
@AndrewGrant 经过进一步思考,我不敢苟同“深拷贝”是一个明确定义的术语。根据您所阅读的资料,深度递归到嵌套数据结构是否是“深拷贝”操作的要求并不清楚。换句话说,对于创建一个新对象,并将其成员作为原始对象成员的浅拷贝进行复制的操作,你会得到不同的答案,是“深拷贝”操作还是不是。请参见https://dev59.com/O2025IYBdhLWcg3wKCf-#6183597以了解此问题的讨论(在Java环境下,但仍然相关)。 - Mark Amery
@AndrewGrant 我必须支持 @MarkAmery 和 @Genericrich 的观点。如果集合中使用的根类和其所有元素都是可复制的,则深拷贝定义良好。这在NSArray(和其他objc集合)中并非如此。如果一个元素没有实现"copy",那么应该放入"深拷贝"中呢?如果这个元素是另一个集合,"copy"实际上并没有产生一个副本(相同的类)。因此,我认为在特定情况下争论所需的副本类型是完全合理的。 - Nikolai Ruhe
如果一个元素没有实现NSCopying/-copy,那么它就是不可复制的——所以你永远不应该尝试复制它,因为这不是它被设计具有的能力。在Cocoa的实现方面,不可复制的对象通常与一些C后端状态相关联,因此直接复制对象可能会导致竞态条件或更糟糕的情况。所以回答“‘深拷贝’中应该放什么”——保留引用。当你有一个非NSCopying对象时,你唯一能够放置的东西。 - Slipp D. Thompson
6个回答

216

正如苹果关于深拷贝的文档明确指出:

如果你只需要一层深度的复制:

NSMutableArray *newArray = [[NSMutableArray alloc] 
                             initWithArray:oldArray copyItems:YES];

上述代码创建了一个新的数组,其成员是旧数组成员的浅拷贝。

请注意,如果您需要深复制整个嵌套数据结构 - 即所链接的苹果文档中称为“真正的深度复制” - 那么这种方法将不足以满足要求。有关此内容,请参阅此处的其他答案。


@quantumpotato:你需要在你的Turn类上实现NSCopying协议。(是的,我意识到这很晚了。) - Wevah
7
我认为这个方法无法如预期般工作。从Apple文档中可以得知:“copyWithZone:”方法执行的是浅拷贝。如果您有一个任意深度的集合,在flag参数中传递YES将对表面下面的第一层执行不可变拷贝。如果传递NO,则第一层的可变性不受影响。在任何情况下,所有更深层次的可变性都不受影响。该SO问题涉及深度可变拷贝。 - Joe D'Andrea
9
这是一个不完整的回答。这会导致一个一级深度的复制。如果数组中有更复杂的类型,它将无法提供一个深度复制。 - Cameron Lowell Palmer
1
因为这是一个NSArray的方法,不是专属于NSMutableArray的。 - Mark Amery
7
这可能是一个深拷贝,这取决于接收类如何实现copyWithZone:方法。 - devios1
显示剩余3条评论

65

我知道的唯一轻松实现此操作的方法是对数组进行归档,然后立即取消归档。这感觉有点像一个技巧,但实际上在苹果文档关于复制集合中明确建议使用,其中指出:

If you need a true deep copy, such as when you have an array of arrays, you can archive and then unarchive the collection, provided the contents all conform to the NSCoding protocol. An example of this technique is shown in Listing 3.

Listing 3 A true deep copy

NSArray* trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:
          [NSKeyedArchiver archivedDataWithRootObject:oldArray]];

需要注意的是,您的对象必须支持NSCoding接口,因为这将用于存储/加载数据。

Swift 2版本:

let trueDeepCopyArray = NSKeyedUnarchiver.unarchiveObjectWithData(
    NSKeyedArchiver.archivedDataWithRootObject(oldArray))

13
如果你的数组很大,使用NSArchiver和NSUnarchiver会在性能方面带来很大的负担。编写一个通用的NSArray分类方法,使用NSCopying协议可以解决这个问题,它会对不可变对象进行简单的'retain'操作,并对可变对象进行'real copy'操作,使得代码更加轻量级。 - Nikita Zhuk
谨慎对待开支是好的,但是相较于initWithArray:copyItems:方法中使用的NSCopying,NSCoding真的更昂贵吗?考虑到有多少控件类符合NSCoding而不符合NSCopying,这种归档/解档的解决方案似乎非常有用。 - Wienke
我强烈建议您永远不要使用这种方法。序列化永远不会比直接复制内存更快。 - Brett
1
如果您有自定义对象,请确保实现encodeWithCoder和initWithCoder方法以符合NSCoding协议。 - user523234
当使用序列化时,显然程序员应该谨慎行事。 - VH-NZZ

35

默认情况下,copy方法会进行浅拷贝

这是因为调用copy方法等同于copyWithZone:NULL,也就是使用默认区域进行拷贝。copy方法不会进行深拷贝。在大多数情况下,它将会给你一个浅拷贝,但是这取决于类的实现。关于这个问题的详细讨论,我建议您查看苹果开发者网站上的集合编程主题

通过initWithArray:CopyItems:方法进行一级深度拷贝

NSArray *deepCopyArray = [[NSArray alloc] initWithArray:someArray copyItems:YES];

NSCoding是苹果推荐的提供深度复制的方式

对于真正的深度复制(数组内嵌数组),您需要使用NSCoding并存档/解档对象:

NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

1
这是正确的。无论流行程度或过早优化的内存、性能边缘情况如何。顺便说一句,这种用于深拷贝的ser-derser hack在许多其他语言环境中都被使用。除非有对象去重,否则这保证了一个完全独立于原始对象的良好深拷贝。 - user246672
1
这是一个不易失效的苹果文档链接:https://developer.apple.com/library/mac/#documentation/cocoa/conceptual/Collections/Articles/Copying.html - user246672

8

对于字典

NSMutableDictionary *newCopyDict = (NSMutableDictionary *)CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)objDict, kCFPropertyListMutableContainers);

对于数组

NSMutableArray *myMutableArray = (NSMutableArray *)CFPropertyListCreateDeepCopy(NULL, arrData, kCFPropertyListMutableContainersAndLeaves);


5
不,框架中没有为此构建的内容。Cocoa集合支持浅复制(使用copyarrayWithArray:方法),但甚至不讨论深复制概念。 这是因为随着您的集合内容开始包含自己的自定义对象,“深复制”开始变得难以定义。“深复制”是否意味着对象图中的每个对象相对于原始对象图中的每个对象都是唯一引用? 如果有某些假设的NSDeepCopying协议,您可以设置它并在所有对象中做出决策,但不幸的是没有。如果您控制了图形中的大多数对象,则可以创建此协议并实现它,但您需要根据需要向Foundation类添加类别。

@AndrewGrant的答案建议使用键控归档/解档来实现任意对象的深度复制,这种方法虽然不够高效,但是正确而干净。这本书甚至建议为所有对象添加一个类别以支持深度复制。


2

如果要实现JSON兼容数据的深度复制,我有一个解决方法。

只需使用NSJSONSerialization获取NSArrayNSData,然后重新创建JSON对象,这将创建一个全新的、拥有它们的新内存引用的NSArray/NSDictionary完整副本。

但是,请确保NSArray/NSDictionary的对象及其子对象必须是可序列化的JSON对象。

NSData *aDataOfSource = [NSJSONSerialization dataWithJSONObject:oldCopy options:NSJSONWritingPrettyPrinted error:nil];
NSDictionary *aDictNewCopy = [NSJSONSerialization JSONObjectWithData:aDataOfSource options:NSJSONReadingMutableLeaves error:nil];

1
您必须在此问题的使用情况下指定NSJSONReadingMutableContainers - Nikolai Ruhe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接