NSMutableDictionary的深拷贝

39
我希望创建一个NSMutableDictionary的深拷贝,并将其赋值给另一个NSMutableDictionary。该字典包含许多数组,每个数组都包含名称,而键是一个字母表(这些名称的第一个字母)。因此,字典中的一个条目是'A' -> 'Adam','Apple'。这是我在一本书中看到的,但我不确定它是否有效:
- (NSMutableDictionary *) mutableDeepCopy
{
    NSMutableDictionary * ret = [[NSMutableDictionary alloc] initWithCapacity: [self count]];
    NSArray *keys = [self allKeys];

    for (id key in keys)
    {
        id oneValue = [self valueForKey:key]; // should return the array
        id oneCopy = nil;

        if ([oneValue respondsToSelector: @selector(mutableDeepCopy)])
        {
            oneCopy = [oneValue mutableDeepCopy];
        }
        if ([oneValue respondsToSelector:@selector(mutableCopy)])
        {
            oneCopy = [oneValue mutableCopy];
        }

        if (oneCopy == nil) // not sure if this is needed
        {   
            oneCopy = [oneValue copy];
        }
        [ret setValue:oneCopy forKey:key];

        //[oneCopy release];
    }
    return ret;
}
  • 是否应该有 [onecopy release]?
  • 我将如何调用这个方法:

    self.namesForAlphabets = [self.allNames mutableDeepCopy];

这样做可以吗? 还是会导致内存泄漏?(假设我将self.namesForAlphabets声明为属性,并在dealloc中释放它)。


你所描述的结构(按照首字母列出姓名的简单通讯录样式列表)并没有什么“深度”,那么为什么你要尝试进行“深拷贝”呢?你需要这个做什么? - Motti Shneor
8个回答

77

由于免费桥接,您还可以使用CoreFoundation函数CFPropertyListCreateDeepCopy

NSMutableDictionary *mutableCopy = (NSMutableDictionary *)CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)originalDictionary, kCFPropertyListMutableContainers);

2
它遵循核心基础框架的“创建”规则,因此您需要确保释放或自动释放返回的字典(或者如果您想保留它,则不要保留它)。 - Wevah
13
只有在属性列表值的情况下,这才有效。如果遇到其他任何内容,它只会返回NULL。 - Ahti
NSNull 不是一个有效的属性列表值,遗憾地。 - Wevah
4
我认为你需要使用CFBridgeRelease。对于一个数组而言,代码应该是这样的:mutableArray = (NSMutableArray *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFArrayRef)oldArrat, kCFPropertyListMutableContainers)); 这会将CF对象转换为Foundation对象,并释放对象所有权,生成一个可变的数组副本。 - honkskillet
1
只有当所有元素都是属性列表对象(字符串、数组、字典、数字、日期、数据)时才能正常工作。你的字典中还有其他对象吗? - Wevah
显示剩余5条评论

13

假设数组的所有元素都实现了NSCoding协议,您可以通过归档来进行深拷贝,因为归档将保留对象的可变性。

像这样:

id DeepCopyViaArchiving(id<NSCoding> anObject)
{
    NSData* archivedData = [NSKeyedArchiver archivedDataWithRootObject:anObject];
    return [[NSKeyedUnarchiver unarchiveObjectWithData:archivedData] retain];
}

不过,这并不是特别有效率的。


3
你放进去什么,就会得到什么。如果你放进一个NSMutableArray,你就会得到一个NSMutableArray。 - Tom Dalling
@dreamlax 如果不是的话,你可以使用 mutableCopy 创建一个可变版本。 - Alex Cio
1
@亚历山大:这只会创建一个可变的“顶级”,但任何嵌套的容器对象仍将是不可变的。 - dreamlax
这不会使任何东西变得不可变 - 可变,这也是问题的目的。 - Motti Shneor
苹果公司在其旧文档中推荐使用此方法创建“真正深层次和全面可变的副本”。较低级别将与原始对象完全相同,因为没有复制本身——只有序列化和反序列化,这保留了原始类的使用。文档链接:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html - Motti Shneor
显示剩余2条评论

10

重要提示:本问题(以及下面我的代码)仅涉及一个非常特定的情况,即NSMutableDictionary 仅包含字符串数组。这些解决方案不适用于更复杂的示例。有关更一般的情况解决方案,请参见以下内容:


针对此特定情况的答案:

您的代码应该可以工作,但是您肯定需要[oneCopy release]。当您使用setValue:forKey添加它们时,新的字典将保留已复制的对象,因此如果您不调用[oneCopy release],所有这些对象都会被保留两次。

一个好的经验法则:如果您allocretaincopy某物,您也必须release它。

注意:这里是一些样例代码,仅在某些情况下有效。这是因为您的NSMutableDictionary仅包含字符串数组(无需进一步进行深度复制):

- (NSMutableDictionary *)mutableDeepCopy
{
    NSMutableDictionary * ret = [[NSMutableDictionary alloc]
                                  initWithCapacity:[self count]];

    NSMutableArray * array;

    for (id key in [self allKeys])
    {
        array = [(NSArray *)[self objectForKey:key] mutableCopy];
        [ret setValue:array forKey:key];
        [array release];
    }

    return ret;
}

1
谢谢,但是“copy”会使NSArray(或NSMutableArray)在新字典中变成不可变的。所以这样做行不通。 - Z S
3
将“copy”替换为“mutableCopy”就可以了。 - Z S
1
for 循环中间还有一个拼写错误 - 将 setValue:copy 替换为 setValue:array - Nik
1
@Krishnabhadra:如果您使用自定义对象,则绝对需要实现NSCopying。原始问题仅涉及NSArray对象。 - e.James
如问题所述,NSDictionary仅包含字符串数组,因此无需担心更深层次的情况。但是,我可以看出一些访问者可能没有意识到这个解决方案仅适用于特殊情况。我将尝试使其更清晰明了。 - e.James
显示剩余2条评论

10

我见过另一种技巧(不太高效),就是使用 NSPropertyListSerialization 对象将你的字典序列化,然后你进行反序列化但指定你想要可变的叶子和容器。


NSString *errorString = nil;
NSData *binData = 
  [NSPropertyListSerialization dataFromPropertyList:self.allNames
                                             format:NSPropertyListBinaryFormat_v1_0
                                        errorString:&errorString];

if (errorString) {
    // Something bad happened
    [errorString release];
}

self.namesForAlphabets = 
 [NSPropertyListSerialization propertyListFromData:binData
                                  mutabilityOption:NSPropertyListMutableContainersAndLeaves
                                            format:NULL
                                  errorDescription:&errorString];

if (errorString) {
    // something bad happened
    [errorString release];
}

再次强调,这种方法并不高效。


2
为什么这个方法不够高效? - Just a coder
因为将数据序列化和反序列化到PLIST中是一个缓慢且占用内存的过程,而其他技术通常使用对象指针进行操作,而不会复制它们的内容。 - Motti Shneor

5
尝试通过检查respondToSelector(@selector(mutableCopy))来确定对象是否可变,这并不能得到所需的结果,因为所有基于NSObject的对象都会对此选择器做出响应(它是NSObject 的一部分)。相反,我们必须查询一个对象是否符合NSMutableCopying或至少NSCopying。以下是我基于接受的答案中提到的此要点的答案:
对于NSDictionary
@implementation NSDictionary (MutableDeepCopy)

//  As seen here (in the comments): https://gist.github.com/yfujiki/1664847
- (NSMutableDictionary *)mutableDeepCopy
{
    NSMutableDictionary *returnDict = [[NSMutableDictionary alloc] initWithCapacity:self.count];

    NSArray *keys = [self allKeys];

    for(id key in keys) {
        id oneValue = [self objectForKey:key];
        id oneCopy = nil;

        if([oneValue respondsToSelector:@selector(mutableDeepCopy)]) {
            oneCopy = [oneValue mutableDeepCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSMutableCopying)]) {
            oneCopy = [oneValue mutableCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSCopying)]){
            oneCopy = [oneValue copy];
        } else {
            oneCopy = oneValue;
        }

        [returnDict setValue:oneCopy forKey:key];
    }

    return returnDict;
}

@end

关于NSArray

@implementation NSArray (MutableDeepCopy)

- (NSMutableArray *)mutableDeepCopy
{
    NSMutableArray *returnArray = [[NSMutableArray alloc] initWithCapacity:self.count];

    for(id oneValue in self) {
        id oneCopy = nil;

        if([oneValue respondsToSelector:@selector(mutableDeepCopy)]) {
            oneCopy = [oneValue mutableDeepCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSMutableCopying)]) {
            oneCopy = [oneValue mutableCopy];
        } else if([oneValue conformsToProtocol:@protocol(NSCopying)]){
            oneCopy = [oneValue copy];
        } else {
            oneCopy = oneValue;
        }

        [returnArray addObject:oneCopy];
    }

    return returnArray;
}

@end

这两种方法的内部逻辑相同,可以提取为一个单独的方法,但出于清晰明了的考虑,我将它们保留在原样。


我觉得这很美妙,试图使用这种技术,而且似乎起作用了......直到它不再起作用。 我需要使用KVC“setValue:forKeyPath:”来修改从“[NSUserDefaults standardDefaults]”检索的字典中的较低级别项目-该方法始终返回不可变对象(沿其层次结构一直返回)。 该技术失败了,因为检索到的NSDictionaries / NSArrays并非真正的NSDictionaries。它们是桥接的_NSCFDictionary对象-它们既不符合您的“mutableDeepCopy”协议也不符合“NSMutableCopying”协议。 因此,代码会出现错误。 我仍在寻找解决方法。 - Motti Shneor

2

对于 ARC - 注意使用 kCFPropertyListMutableContainersAndLeaves 来实现真正的深度可变性。

    NSMutableDictionary* mutableDict = (NSMutableDictionary *)
      CFBridgingRelease(
          CFPropertyListCreateDeepCopy(kCFAllocatorDefault, 
           (CFDictionaryRef)someNSDict, 
           kCFPropertyListMutableContainersAndLeaves));

1
如果你正在使用ARC,我会更新一个答案。
Weva提供的解决方案很好。现在你可以像这样做:
NSMutableDictionary *mutableCopy = (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)originalDict, kCFPropertyListMutableContainers));

2
kCFPropertyListMutableContainersAndLeaves 或许也是你想要的,它可以使得可变性更深入。 - Tom Andersen

0

这里有一些有用的答案,但是CFPropertyListCreateDeepCopy在处理数据时不能处理[NSNull null],而这在JSON解码数据中非常普遍。

我正在使用这个类别:

    #import <Foundation/Foundation.h>

    @interface NSObject (ATMutableDeepCopy)
    - (id)mutableDeepCopy;
    @end

实现(可以自由修改/扩展):

    @implementation NSObject (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        return [self copy];
    }

    @end

    #pragma mark - NSDictionary

    @implementation NSDictionary (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        return [NSMutableDictionary dictionaryWithObjects:self.allValues.mutableDeepCopy
                                                  forKeys:self.allKeys.mutableDeepCopy];
    }

    @end

    #pragma mark - NSArray

    @implementation NSArray (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        NSMutableArray *const mutableDeepCopy = [NSMutableArray new];
        for (id object in self) {
            [mutableDeepCopy addObject:[object mutableDeepCopy]];
        }

        return mutableDeepCopy;
    }

    @end

    #pragma mark - NSNull

    @implementation NSNull (ATMutableDeepCopy)

    - (id)mutableDeepCopy
    {
        return self;
    }

    @end

示例扩展 - 字符串保留为普通副本。如果您想要能够就地编辑它们,可以覆盖此行为。我只需要在某些测试中操纵一个深层字典,因此我尚未实现该功能。


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