deep mutable copy of a NSMutableDictionary
Asked Answered
A

8

39

I am trying to create a deep-copy of a NSMutableDictionary and assign it to another NSMutableDictionary. The dictionary contains a bunch of arrays, each array containing names, and the key is an alphabet (the first letter of those names). So one entry in the dictionary is 'A' -> 'Adam', 'Apple'. Here's what I saw in a book, but I'm not sure if it works:

- (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;
}
  • should the [onecopy release] be there or not?
  • Here's how I'm going to call this method:

    self.namesForAlphabets = [self.allNames mutableDeepCopy];

Will that be ok? Or will it cause a leak? (assume that I declare self.namesForAlphabets as a property, and release it in dealloc).

Aghast answered 23/12, 2009 at 2:22 Comment(1)
There is nothing "deep" in the structure you describe (a simple address-book style list of names by their first letter) -- so why you even attempt a "deep copy" ? what do you need this for?Monge
C
10

IMPORTANT: The question (and my code below) both deal with a very specific case, in which the NSMutableDictionary contains only arrays of strings. These solutions will not work for more complex examples. For more general case solutions, see the following:


Answer for this specific case:

Your code should work, but you will definitely need the [oneCopy release]. The new dictionary will retain the copied objects when you add them with setValue:forKey, so if you do not call [oneCopy release], all of those objects will be retained twice.

A good rule of thumb: if you alloc, retain or copy something, you must also release it.

Note: here is some sample code that would work for certain cases only. This works because your NSMutableDictionary contains only arrays of strings (no further deep copying required):

- (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;
}
Conception answered 23/12, 2009 at 2:37 Comment(7)
Thanks, but the "copy" makes the NSArray (or NSMutableArray) immutable in the new dictionary. So that's not going to work.Aghast
replaced 'copy' with 'mutableCopy' and it's fine.Aghast
Ah, yes. Sorry! That should definitely have been mutableCopy. I made the change in my answer.Conception
There is also a typo in the middle of for loop - replace setValue:copy with setValue:arrayKancler
@Conception do we need to have NSCopying for this function to work? I am getting error -[MyClass copyWithZone:]: unrecognized selector sent to instanceCuster
@Krishnabhadra: Yes, you will definitely need to implement NSCopying if you are using custom objects. The original question dealt only with NSArray objects.Conception
@hfossli: as stated in the question, the NSDictionary contains only arrays of strings, so there is no need to worry about deeper levels. However, I can see that some visitors might not realize that this solution only works in special cases. I will attempt to make it more clear.Conception
I
77

Because of toll-free bridging, you can also use the CoreFoundation function CFPropertyListCreateDeepCopy:

NSMutableDictionary *mutableCopy = (NSMutableDictionary *)CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)originalDictionary, kCFPropertyListMutableContainers);
Inflation answered 23/12, 2009 at 5:56 Comment(10)
Cool. This works great. Except, for some reason it registers as a leak in performance tool. Any idea what that's about?Deidredeific
It follows Core Foundation's "create" rule, so you need to make sure you release or autorelease the returned dictionary (or just not retain it if you want to keep it around).Inflation
So it seems you'd have to call: CFRelease(mutableCopy); when you need to release the object. Thanks!Deidredeific
I think you want to cast the NSDictionary as a CFDictionaryRef, not as CFDictionary, in the 2nd argument to CFPropertyListCreateDeepCopy.Ferrel
this only works if there are only property-list-values though. if it encounters anything else it just returns NULLLibby
NSNull isn't a valid property list value, unfortunately.Inflation
I think you have to use CFBridgeRelease. For an array it is, mutableArray = (NSMutableArray *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFArrayRef)oldArrat, kCFPropertyListMutableContainers));Schnitzel
This didn't work for me. Looks like there are more details you have to be careful when using it.Picro
It only works if all the elements are property list objects (strings, arrays, dictionaries, numbers, dates, data). Do you have any other objects in your dictionary?Inflation
Wonderful. That's what I was looking for - I'm trying to use KVC to modify lower-level items in [NSUserDefaults standardDefaults]. The hierarchy retrieved is all immutable, and techniques involved with extending NSDictionary and NSArray don't work - since the underlying objects seem to be all of the _NSCFDictionary set of apple private classes. They will hopefully yield to CFPropertyListCreateDeepCopy better.Monge
C
13

Assuming all elements of the array implement the NSCoding protocol, you can do deep copies via archiving because archiving will preserve the mutability of objects.

Something like this:

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

This isn't particularly efficient, though.

Cloakroom answered 23/12, 2009 at 3:0 Comment(7)
Does this method return a mutable deep copy or an immutable deep copy?Psychedelic
Whatever you put into it. If you put in an NSMutableArray, you get back an NSMutableArray.Cloakroom
@Psychedelic if not, you could just create a mutable version of it with mutableCopyPicro
@亚历山大: That would just create a mutable "top-level" but any nested container objects would still be immutable.Psychedelic
Ok, sorry your right. In my case, I didn't have any nested containers which needed to be mutable.Picro
That won't make anything Immutable - mutable, which is the purpose of the question.Monge
Apple recommends this way for creating "real deep and all-level mutable copies" in their old documentation. Lower levels WILL be mutable/immutable exactly as the original, because there is no copying per se - there is serializing and de-serializing which preserves that original classes used. Docs: developer.apple.com/library/archive/documentation/Cocoa/…Monge
C
10

IMPORTANT: The question (and my code below) both deal with a very specific case, in which the NSMutableDictionary contains only arrays of strings. These solutions will not work for more complex examples. For more general case solutions, see the following:


Answer for this specific case:

Your code should work, but you will definitely need the [oneCopy release]. The new dictionary will retain the copied objects when you add them with setValue:forKey, so if you do not call [oneCopy release], all of those objects will be retained twice.

A good rule of thumb: if you alloc, retain or copy something, you must also release it.

Note: here is some sample code that would work for certain cases only. This works because your NSMutableDictionary contains only arrays of strings (no further deep copying required):

- (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;
}
Conception answered 23/12, 2009 at 2:37 Comment(7)
Thanks, but the "copy" makes the NSArray (or NSMutableArray) immutable in the new dictionary. So that's not going to work.Aghast
replaced 'copy' with 'mutableCopy' and it's fine.Aghast
Ah, yes. Sorry! That should definitely have been mutableCopy. I made the change in my answer.Conception
There is also a typo in the middle of for loop - replace setValue:copy with setValue:arrayKancler
@Conception do we need to have NSCopying for this function to work? I am getting error -[MyClass copyWithZone:]: unrecognized selector sent to instanceCuster
@Krishnabhadra: Yes, you will definitely need to implement NSCopying if you are using custom objects. The original question dealt only with NSArray objects.Conception
@hfossli: as stated in the question, the NSDictionary contains only arrays of strings, so there is no need to worry about deeper levels. However, I can see that some visitors might not realize that this solution only works in special cases. I will attempt to make it more clear.Conception
P
10

Another technique that I have seen (which is not at all very efficient) is to use an NSPropertyListSerialization object to serialise your dictionary, then you de-serialise it but specify that you want mutable leaves and containers.


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];
}

Again, this is not at all efficient.

Psychedelic answered 23/12, 2009 at 2:57 Comment(2)
why is this method inefficient?Devorahdevore
because serialization and de-serialization into PLIST are slow memory-consuming processes, while the other techniques generally play with pointers to objects, not duplicating their contents.Monge
S
5

Trying to figure out by checking respondToSelector(@selector(mutableCopy)) won't give the desired results as all NSObject-based objects respond to this selector (it's part of NSObject). Instead we have to query if an object conforms to NSMutableCopying or at least NSCopying. Here's my answer based on this gist mentioned in the accepted answer:

For 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

For 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

Both methods have the same internal to-copy-or-not-to-copy logic and that could be extracted into a separate method but I left it like this for clarity.

Sternlight answered 7/11, 2014 at 3:8 Comment(1)
I thought this beautiful, Tried to use the technique, and it seemed to work... until it didn't. I need to use KVC 'setValue:forKeyPath:' to modify lower-level items in a dictionary retrieved from '[NSUserDefaults standardDefaults]' - which always returns immutable objects (all the way down their hierarchy). The technique failed, because the NSDictionaries/NSArrays retrieved - aren't really NSDictionaries. They are bridged _NSCFDictionary objects - which don't conform to either your 'mutableDeepCopy' protocol, nor to 'NSMutableCopying' protocol. So the code breaks. I'm still looking for a cure.Monge
E
2

For ARC - note kCFPropertyListMutableContainersAndLeaves for truly deep mutability.

    NSMutableDictionary* mutableDict = (NSMutableDictionary *)
      CFBridgingRelease(
          CFPropertyListCreateDeepCopy(kCFAllocatorDefault, 
           (CFDictionaryRef)someNSDict, 
           kCFPropertyListMutableContainersAndLeaves));
Endres answered 23/9, 2015 at 21:38 Comment(0)
L
1

Thought I'd update with an answer if you're using ARC.

The solution Weva has provided works just fine. Nowadays you could do it like this:

NSMutableDictionary *mutableCopy = (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)originalDict, kCFPropertyListMutableContainers));
Lafrance answered 3/9, 2015 at 9:24 Comment(1)
kCFPropertyListMutableContainersAndLeaves is maybe also what you want, which makes the mutability deeper.Endres
J
0

Useful answers here, but CFPropertyListCreateDeepCopy doesn't handle [NSNull null] in the data, which is pretty normal with JSON decoded data, for example.

I'm using this category:

    #import <Foundation/Foundation.h>

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

Implementation (feel free to alter / extend):

    @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

Example extensions – strings are left as normal copies. You could override this if you want to be able to in place edit them. I only needed to monkey with a deep down dictionary for some testing, so I've not implemented that.

Jopa answered 3/1, 2017 at 16:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.