Suppose I am holding data in an array like this
wordList = [[NSMutableArray alloc] init];
while ([rs next]) //Some database return loop
{
wordDict = [[NSMutableDictionary alloc] init];
[wordDict setObject:[NSNumber numberWithInt:[rs intForColumn:#"id"]] forKey:#"id"];
[wordDict setObject:[rs stringForColumn:#"word"] forKey:#"word"];
[wordList addObject: wordDict];
[wordDict release];
wordDict = nil;
}
But I want to store this result (i.e. wordList) in SQLite for later use - I guess using NSCoding. How would I do that?
(Feel free to point out any errors in how stuff is being alloc'ed if there are problems there).
If you don’t insist on serialization using NSCoding, there’s a writeToFile:atomically: method both on NSArray and NSDictionary. This will serialize your object into a property list (*.plist). The only catch is that all the objects in the “tree” to be serialized must be NSString, NSData, NSArray, or NSDictionary (see the documentation). I’m not sure how NSNumber fits in, but with a bit of luck it will be serialized and deserialized too. The inverse method that will turn the file back into a dictionary or an array is called initWithContentsOfFile:.
As for your code, I would just use the [NSMutableDictionary dictionary] convenience method that gets you an autoreleased dictionary. It’s shorter than the usual alloc & init and you save one line for the explicit release.
Related
In the following code, I don't get any warning or compile error if I return NSMutableArray instead of NSArray which is the method's return type.
But is it good practice to convert return value to NSArray by using -copy method like the following code?
Or should I return NSMutableArray?
+(NSArray *)returnImutableArray {
NSMutableArray *arr = [[NSMutableArray alloc] init];
[arr addObject:#"a"];
[arr addObject:#"b"];
//return arr; // no warning
return [arr copy];
}
I think this is essentially personal preference. You incur a (probably small) performance penalty by copying the array before returning it. The upside is that you'll completely prevent mutation of the result further down the line. However, I don't usually go to the trouble. Because the declared return type of the method is an immutable NSArray, the compiler will warn you if you try to call one of NSMutableArray's mutation methods on it unless you go out of your way to prevent that (by casting to NSMutableArray).
So, in short, it's personal preference and I personally don't generally bother with the immutable copy.
This all depends on what you want to achieve with the array.
If you really don't want or need to modify the returned array, return it as an NSArray.
You could as well return it as an array, without copying it.
In both cases, don't forget the memory management.
You should never return anything with a retain count >0.
+(NSArray *)returnImutableArray {
NSMutableArray *arr = [[NSMutableArray alloc] init];
[arr addObject:#"a"];
[arr addObject:#"b"];
NSArray * returnArray = [NSArray arrayWithArray:arr];
[arr release];
//return arr; // no warning
return returnArray;
}
Or you could do so:
+(NSArray *)returnImutableArray {
NSMutableArray *arr = [[NSMutableArray alloc] init];
[arr addObject:#"a"];
[arr addObject:#"b"];
//return arr; // no warning
return [arr autorelease];
}
There's no need to copy the NSMutableArray as it extends the NSArray so your code will never break if you do so.
I return an immutable copy of interior mutable instances by default. Not just NSArray, but several other types.
I don't want mutable objects floating around, although it's not a problem that it mutates as long as you treat it as concrete.
I consider it a better default because it minimizes shallow copying if shared. That is, if you take the result and assign it to one or more objects which are copy properties, then all those actions must make concrete shallow copies, when they could effectively retain. This is the same reason you should favor copy for properties of these types, rather than retain when declaring properties.
In some cases, you just know its scope is limited and that the mutable won't get out -- in that case, I may not bother making the copy.
I'm having trouble in writing mutable dictionary to a file. Here's the code that I'm writing.
I'm reading the file like below: (first time when app is ran, it won't have any data)
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
self.favSaveFilePath = [[NSString alloc] initWithString:[documentsDirectory stringByAppendingPathComponent:#"Favorites.plist"]];
if([fm fileExistsAtPath:favSaveFilePath])
{
favoriteJokesDictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:self.favSaveFilePath];
favoriteJokes = [[NSMutableArray alloc] initWithArray:[self.favoriteJokesDictionary objectForKey:#"FavoriteJokes"]];
return;
}
I'm adding new dictionary to array as below:
-(BOOL)addJokeToFavorite:(NSDictionary*)joke
{
BOOL success = NO;
[self.favoriteJokes addObject:joke];
success = [self.favoriteJokesDictionary writeToFile:self.favSaveFilePath atomically:YES];
return success;
}
I don't know why its not wring the dictionary to file. Can any one point me the mistake that I'm doing?
The Cocoa API is very specific about what kind of values can legally be in a dictionary when it is written out to file. One particular limitation that has been known to cause problems and which is not thoroughly discussed in the official API documentation is that all of the keys in your dictionary must be of type NSString (and your values must be one of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary), even though the dictionary itself supports keys and values of any object type.
The reason for this restriction has to do with the fact that the dictionary gets stored as an XML document (or "property-list", in Apple-speak) by default. So I'd recommend verifying that your dictionary meets the requirements to be saved to file, and restructuring it if it doesn't.
Or, if restructuring your dictionary is not feasible, you might want to take a look at the NSKeyedArchiver class instead.
Looks like your favoriteJokesDictionary doesn't exist at all when there's no save file to initialize it from. Hence, the attempt to create that save file from the non-existing dictionary in addJokeToFavorite: doesn't do anything, either. You need to create an empty dictionary when there is no save file to begin with.
I was just typing this up when Ulrich posted the correct answer. I'll add the rest here for extra clarification.
As Ulrich pointed out, when you don't have a file, your code just skips the initialization of favoriteJokesDictionary and favoriteJokes. When you try to write out the object later, favoriteJokesDictionary is nil and so the method doesn't do anything.
You can just create an empty NSMutableDictionary if the file doesn't exist yet, and since you're retaining part of the dictionary in a separate ivar as well you should create that at the same time. Something like this would work:
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
self.favSaveFilePath = [[NSString alloc] initWithString:[documentsDirectory stringByAppendingPathComponent:#"Favorites.plist"]];
if([fm fileExistsAtPath:favSaveFilePath])
{
favoriteJokesDictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:self.favSaveFilePath];
favoriteJokes = [[NSMutableArray alloc] initWithArray:[self.favoriteJokesDictionary objectForKey:#"FavoriteJokes"]];
} else {
favoriteJokesDictionary = [[NSMutableDictionary alloc] init];
favoriteJokes = [[NSMutableArray alloc] init];
[favoriteJokesDictionary setObject:favoriteJokes forKey:#"JokesArrayKey"];
}
That said, I think you could probably simplify your data model there a bit (unless there much more going on that you're not showing). If you need a dictionary because you're storing more than just jokes, then what you have is fine. However, I wouldn't retain the jokes array, because if you later set a new object in the array for your JokesArrayKey, your favoriteJokes ivar is still going to be pointing to the old array.
I would just grab the object from the dictionary instead whenever you need it:
NSMutableArray *jokes = [favoriteJokesDictionary objectForKey:#"JokesArrayKey"];
Even better, if you are working with the jokes array quite a bit, just pull it out into its own file. You can write an NSArray out as a plist just like you can with an NSDictionary. If you're only using the NSDictionary as a shell to write out a .plist, you can skip it entirely.
One other thing: Assuming your favSaveFilePath property is marked "retain", you have a small memory leak in the first section. You're creating a new retained string with [[NSString alloc] initWithString:...], and then retaining it again by assigning it to your property. You probably just want:
self.favSaveFilePath = [documentsDirectory stringByAppendingPathComponent:#"Favorites.plist"];
Referring to the answer aroth gave, non-ns objects in your dictionary may be more subtle than you realise.
EG: I had an NSArray of NSDictionary objects that I was turning into NSObject subclassed items, and adding these to a NSMutableDictionary.
Even though the subclass only contained NS based objects, strings etc; the NSMutableDictionary itself wouldn't save.
The solution was to save the NSArray of NSDictionary items and turn them into my custom subclass after loading the file.
In the following piece of code (from a book) data is an NSDictionary *data; defined in the header (no property).
In the viewDidLoad of the controller the following occurs:
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *keys = [NSArray arrayWithObjects:#"home", #"work", nil];
NSArray *homeDVDs = [NSArray arrayWithObjects:#"Thomas the Builder", nil];
NSArray *workDVDs = [NSArray arrayWithObjects:#"Intro to Blender", nil];
NSArray *values = [NSArray arrayWithObjects:homeDVDs, workDVDs, nil];
data = [[NSDictionary alloc] initWithObjects:values forKeys:keys];
}
Since I am really new to objective-c can someone explain to me why I do not have to retain the variables keys,homeDVDs,workDVDs and values prior exiting the function? I would expect prior the data allocation something like:
[keys retain];
[homeDVDs retain];
[workDVDs retain];
[values retain];
or not? Does InitWithObjects copies (recursively) all objects into a new table?
Assuming we did not have the last line (data allocation) should we release all the NSArrays prior exiting the function (or we could safely assumed that all NSArrays will be autoreleased since there is no alloc for each one?)
Thanks!!!!
In Core Foundation, most of static calls to methods such as arrayWithObjects: return autoreleased instances, which means you don't need to (and even must not!) release it by yourself.
So the situation after your code is executed is that all arrays you created with arrayWithObjects: calls are autoreleased, but are retained when added to other array or dictionary. So homeDVDs and workDVDs are retained when added to the values array, and keys and values arrays are both retained when added to the data dictionary.
So you don't need to explicitly release your arrays, but you'll need to release your data dictionary at some point (perhaps in dealloc method implementation).
NSDictionary copies the key field and retains the values. All your other instances are autoreleased, so you're not missing any releases or retains (assuming you release data in the dealloc method).
I'm assuming my understanding of how to perform a deep copy isn't just there yet. The same with some sub-optimal memory handling that I'm performing down below. This code below probably depicts a shallow copy, and I believe that's where my problem might be. I have some cookie-cutter code for an example that looks like the following:
NSArray *user = [[xmlParser createArrayWithDictionaries:dataAsXML
withXPath:kUserXPath] retain];
if([user count] > 0) {
self.name = [[user valueForKey:#"name"] copy];
}
// Crash happens if I leave the next line un-commented.
// But then we have a memory leak.
[user release];
[xmlParser release];
Unfortunately when I comment out [user release], the code works, but we have an obvious memory leak. The method createArrayWithDictionaries:withXPath: was refactored last night when the SO community helped me understand better memory management. Here's what it looks like:
- (NSArray *)createArrayWithDictionaries:(NSString *)xmlDocument
withXPath:(NSString *)XPathStr {
NSError *theError = nil;
NSMutableArray *dictionaries = [NSMutableArray array];
CXMLDocument *theXMLDocument = [CXMLDocument alloc];
theXMLDocument = [theXMLDocument initWithXMLString:xmlDocument
options:0
error:&theError];
NSArray *nodes = [theXMLDocument nodesForXPath:XPathStr error:&theError];
for (CXMLElement *xmlElement in nodes) {
NSArray *attributes = [xmlElement attributes];
NSMutableDictionary *attributeDictionary;
attributeDictionary = [NSMutableDictionary dictionary];
for (CXMLNode *attribute in attributes) {
[attributeDictionary setObject:[attribute stringValue]
forKey:[attribute name]];
}
[dictionaries addObject:attributeDictionary];
}
[theXMLDocument release];
return dictionaries;
}
I'm guessing there's a couple of issues that might be going on here:
Auto release on my dictionaries array is happening, thus my app crashing.
I'm not performing a deep copy, only a shallow copy. Thus when the user array is released, self.name is done for.
With NSZombieEnabled, I see the following:
*** -[CFString respondsToSelector:]:
message sent to deallocated instance 0x1ae9a0
Also, the final call where the backtrace shows this is crashing contains the following code in a separate module from the other two methods:
User *u = self.user;
NSString *uri = [NSString stringWithFormat:#"%#/user/%#/%#",
[self groupName], u.userId, kLocationsUri];
Between all the auto releasing/copies/retain happening between the client code and createArrayWithDictionaries:withXPath, I'm a bit confused as to the real problem here. Thanks again for helping me understand.
OK, you don't need to retain the return value from createArrayWithDictionaries: since you're not keeping it around. The return value is autoreleased. I'd strongly recommend reading up on how autoreleasing works. You only retain things that you intend to keep around in your object.
Also, user is an NSArray. If you call [user valueForKey:#"name"], you'll get another NSArray of values representing the values of the name key for each of the objects in users. Furthermore, how is the name property on your object defined? If you declared it as copy or retain (I believe retain is the default if you don't specify it yourself), you don't need to copy or retain the value. Indeed, the accessor should always be responsible for doing the memory management, not the caller. If you wrote your own accessor (i.e. you didn't use the #synthesize keyword), you need to make sure you do the memory management there.
I'm guessing what you meant to write was something more like this:
NSArray *user = [xmlParser createArrayWithDictionaries:dataAsXML withXPath:kUserXPath];
if ([user count] > 0)
self.name = [[user objectAtIndex:0] objectForKey:#"name"];
[xmlParser release];
I think your troubles are stemming from a misunderstanding of how memory management works in Objective-C.
Hope this helps.
Auto release on my dictionaries array is happening, thus my app crashing.
If the caller intends to keep the array around somewhere, it needs to retain it. Otherwise, it will crash when it tries to access the (now-deceased) object.
If the caller is going to store it in a property, it must use the self.dictionaries = […] syntax, not dictionaries = […]. The former is a property access, which calls the setter method; the latter is a direct instance variable assignment.
Coming back to your actual question, that of a deep copy: You need to get the sub-elements of every element and put them in each element's dictionary.
Basically, you need a recursive method (or a queue, but that's harder—file under premature optimization until you've proven you need it) that takes an element and returns a dictionary, and then you need to call this method on each of your element's child elements, and collect the results into an array and put that into the dictionary you're creating.
I would recommend making this recursive method an instance method of the element. Something like:
- (NSDictionary *) dictionaryRepresentation {
NSMutableDictionary *attributeDictionary = [NSMutableDictionary dictionary];
for (CXMLNode *attribute in attributes) {
[attributeDictionary setObject:[attribute stringValue] forKey:[attribute name]];
}
NSArray *childElements = [self childElements];
return [NSDictionary dictionaryWithObjectsAndKeys:
attributeDictionary, #"attributes",
[childElements valueForKey:#"dictionaryRepresentation"], #"childElements",
nil];
}
Then you replace the loop in createArrayWithDictionaries:withXPath: with a similar valueForKey: message. I'll leave you to fill it in.
valueForKey: is Key-Value Coding's principal method. In both places, we're making use of NSArray's handy implementation of it.
(If the use of valueForKey: still doesn't make sense to you, you should read the KVC Programming Guide. KVC is vitally important in modern Cocoa, so you do need to read this sooner or later.)
I'm trying hard to understand when and what I must relase in Cocoa Touch as it doesn't have garbage collection.
This code block is from apples iphone sample PeriodicElements and they release anElement and rawElementArray but not thePath, firstLetter, existingArray and tempArray?
I would have thought that at least tempArray and existingArray should be released.
Could some brainy person please explain to me why?
Thanks :)
- (void)setupElementsArray {
NSDictionary *eachElement;
// create dictionaries that contain the arrays of element data indexed by
// name
self.elementsDictionary = [NSMutableDictionary dictionary];
// physical state
self.statesDictionary = [NSMutableDictionary dictionary];
// unique first characters (for the Name index table)
self.nameIndexesDictionary = [NSMutableDictionary dictionary];
// create empty array entries in the states Dictionary or each physical state
[statesDictionary setObject:[NSMutableArray array] forKey:#"Solid"];
[statesDictionary setObject:[NSMutableArray array] forKey:#"Liquid"];
[statesDictionary setObject:[NSMutableArray array] forKey:#"Gas"];
[statesDictionary setObject:[NSMutableArray array] forKey:#"Artificial"];
// read the element data from the plist
NSString *thePath = [[NSBundle mainBundle] pathForResource:#"Elements" ofType:#"plist"];
NSArray *rawElementsArray = [[NSArray alloc] initWithContentsOfFile:thePath];
// iterate over the values in the raw elements dictionary
for (eachElement in rawElementsArray)
{
// create an atomic element instance for each
AtomicElement *anElement = [[AtomicElement alloc] initWithDictionary:eachElement];
// store that item in the elements dictionary with the name as the key
[elementsDictionary setObject:anElement forKey:anElement.name];
// add that element to the appropriate array in the physical state dictionary
[[statesDictionary objectForKey:anElement.state] addObject:anElement];
// get the element's initial letter
NSString *firstLetter = [anElement.name substringToIndex:1];
NSMutableArray *existingArray;
// if an array already exists in the name index dictionary
// simply add the element to it, otherwise create an array
// and add it to the name index dictionary with the letter as the key
if (existingArray = [nameIndexesDictionary valueForKey:firstLetter])
{
[existingArray addObject:anElement];
} else {
NSMutableArray *tempArray = [NSMutableArray array];
[nameIndexesDictionary setObject:tempArray forKey:firstLetter];
[tempArray addObject:anElement];
}
// release the element, it is held by the various collections
[anElement release];
}
// release the raw element data
[rawElementsArray release];
// create the dictionary containing the possible element states
// and presort the states data
self.elementPhysicalStatesArray = [NSArray arrayWithObjects:#"Solid",#"Liquid",#"Gas",#"Artificial",nil];
[self presortElementsByPhysicalState];
// presort the dictionaries now
// this could be done the first time they are requested instead
[self presortElementInitialLetterIndexes];
self.elementsSortedByNumber = [self presortElementsByNumber];
self.elementsSortedBySymbol = [self presortElementsBySymbol];
}
They create rawElementsArray by sending +alloc to the class, therefore this object is owned by the code in the sample above and must be released. Similarly with anElement. Note that thePath and tempArray are not created by sending +alloc, +new or -copy messages, therefore the calling code is not responsible for the lifetime of those objects. Please have a look at this collection of Cocoa memory management articles:
http://iamleeg.blogspot.com/2008/12/cocoa-memory-management.html
The reason you don't have to release tempArray is because it's been allocated and then autoreleased right away. Autorelease is a method of scheduling a release call sometime in the future, so that the caller of an API doesn't have to do any explicit releasing of the result.
Matt Dillard has provided a detailed explanation of Objective C's memory management strategy and has explained it much better than I can.
The convention is that when you create an object using a class method it should have been autoreleased. This means that at the end of the run loop when the autorelease pool is flushed these objects will be released. However, if you create something using +alloc] -init] or -copy, -mutableCopy or +new (which is the same as +alloc] -init]) then it will not have been autoreleased.
For example:
NSArray *array1 = [NSArray arrayWithObject:#"foo"];
NSArray *array2 = [[NSArray alloc] initWithObject:#"foo"];
Array1 will be autoreleased and you don't need to worry about it. Array2 will need to be manually released. Or alternatively you could do:
NSArray *array2 = [[[NSArray alloc] initWithObject:#"foo"] autorelease];
Which is pretty much what +arrayWithObject: does.
Of course this leads to an important consideration with the lifetime of instance variables. If you create the instance variable as with array2 then it will be fine as it has a retain count of 1. However, array1 will need to be retained otherwise it will be autoreleased at the end of the runloop, giving it a retain count of 0 and so it will be freed and you will be left with a dangling pointer.