Apple consciously separates mutable and immutable variants of classes in Objective-C. If you have an NSString you cannot modify it, only by creating a new one with additions to an old one. If you want to mutilate mutate the string itself you have to use it’s subclass NSMutableString. Internally those are the same CoreFoundation type, yet the design choice was to have an immutable class and add mutability methods in a subclass.
There are two items that are not immediately obvious if you start out learning to program Objective-C, that I’d like to chronicle. One of these stumped me just a few days ago.
Take for example the two methods to manipulate a string.
NSString *text = @"Constant String"; text = [text stringByAppendingString:@", really constant?"]; NSLog(@"%@", text); |
You see that you can reuse the text variable and don’t have to create a new NSString pointer. This works because the first string is statically allocated by the compiler as part of the compiled code. The @”” is the shorthand for that. We have to assign the return value of stringByAppendingString to the pointer variable because this is a new NSString instance, marked as autoreleased.
How long do these strings live? The constant one stays around until the app gets unloaded from memory, i.e. when it is terminated. Though if you don’t hold onto the pointer to the static allocation then this amounts to a leak, the memory stays lost but you have no way to access it any more. But there’s nothing you can do about that. The autoreleased new string object will live until the next run loop, which is probably at the end of the method inside which you have place the code.
The mutable alternative:
NSMutableString *text = [NSMutableString stringWithString:@"Constant String"]; [text appendString:@", really constant?"]; NSLog(@"%@", text); |
You see that appendString does not return a value because we don’t create a new object. Instead text itself is mutated. You still see the @”” because there is no shorthand to create an NSMutableString. So we still end up with “Constant String” statically allocated in memory and a new NSMutableString instance. But in this case this new instance is already created in the first line through the stringWithString. The following appendString does not create a new instance but mutates text.
What would the difference be if you had also declared text as NSString * in the second example? Not much, you would get a compiler warning that NSString might not have a appendString method because the compiler goes by the declaration. At runtime it’s still a pointer to an NSMutableString that ends up written into the variable, so messaging that with appendString works without problem. Also, since NSMutableString is a subclass of NSString you can use it anywhere you would need an NSString parameter, the additional mutability methods are irrelevant and will be ignored.
So much for a quick reminder about mutability basics. Besides of using these methods that create autoreleased new instances you can also copy objects. This is the first thing that somebody needs to tell you because it is counterintuitive. What does the following code produce?
NSMutableString *text = [NSMutableString stringWithString:@"Constant Text"]; id newText = [text copy]; |
You might assume that newText is now a mutable copy of text, but that would be wrong. copy always makes immutable copies, even if the original was mutable. To make a mutable copy use the mutableCopy method.
[NSString copy] = NSString
[NSMutableString copy] = NSString
[NSString mutableCopy] = NSMutableString
[NSMutableString mutableCopy] = NSMutableString
Anything with init, new or copy in the method name will not be autoreleased. So you have to make sure that you release it before you leave the current scope or else this will produce a leak, this time a preventable one.
When I started out to program Objective-C I made all synthesized properties on classes retain. But then you find – when looking at the documentation or Apple’s headers – that often properties are not retain, but copy instead. The reason being that a retained property is just a reference to an instance created earlier. If it’s an NSMutableString you pass into a method expecting an NSString that works nicely, but you can still mutate the string outside of the class. Which might lead to unexpected results.
Because of this any property there you want the “value” at this present time to be passed, you should make a copy instead. This copy is a snapshot at the time when the property was used to set the instance variable and no unexpected effects happen if you mutate the parameter afterwards. If you want a reference to the original and know for certain that you also will want to have access to subsequent updates of the passed instance, then retain will be your choice.
@interface MyClass : NSObject { NSString *_name; NSDateFormatter *_birthdayDateFormatter; } @property (nonatomic, copy) NSString *name; @property (nonatomic, retain) NSDateFormatter *birthdayDateFormatter; @end |
Both would have a release in the dealloc method. Usually you would simply @synthesize the methods for the properties defined above. But let’s look at how those would be written manually. Since I don’t like to rename the parameter of an overwritten setter, I usually prefix my instance variables with _. So you can imagine a @synthesize name = _name, property name equals instance variable name.
In this example we want to have the value of the name passed and preserved in our MyClass. But the dateFormatter might get a different format or locale during the runtime of our app, so we only retain it and don’t make a copy. Bear in mind, that to be able to copy any object the NSCopying protocol needs to be implemented. This means you have to implement the copyWithZone: for your own classes to support copy method. For mutableCopy the method to implement is in the NSMutableCopying protocol, i.e. mutableCopyWithZone.
The case where I run into an unexpected error was when I had an NSMutableArray instance variable that I wanted to also copy to copies of this class. Those lost mutability and when I tried to add items later I got the usual unrecognizedSelector exception.
#import "MyClass.h" @implementation MyClass - (void)dealloc { [_name release]; [_birthdayDateFormatter release]; [super dealloc]; } #pragma mark Properties - (void)setName:(NSString *)name { if (name != _name) { [_name release]; _name = [name copy]; } } - (void)setBirthdayDateFormatter:(NSDateFormatter *)birthdayDateFormatter { if (birthdayDateFormatter != _birthdayDateFormatter) { [_birthdayDateFormatter release]; _birthdayDateFormatter = [birthdayDateFormatter retain]; } } @synthesize name = _name; @synthesize birthdayDateFormatter = _birthdayDateFormatter; @end |
This is about the code that the @synthesize would create for you. The if serves to not release the ivar if it is the same because this might free the object. I’ve also seen some people ditch the if and instead use an autorelease, but that just feels wrong to me. Writing less code is not always a good thing.
You can see that the only difference here is that one uses a retain, the other a copy to preserve the object while it’s being used by MyClass. We still have the synthesizes because those create the getters for us, but these are the same for retain, copy and assign.
Now you know why mutability gets lost when you assign a mutable object to a copy property. Which was exactly what I did on the copyWithZone: implementation of DTCoreTextParagraphStyle.
#pragma mark Copying - (id)copyWithZone:(NSZone *)zone { DTCoreTextParagraphStyle *newObject = [[DTCoreTextParagraphStyle allocWithZone:zone] init]; newObject.firstLineIndent = self.firstLineIndent; newObject.defaultTabInterval = self.defaultTabInterval; newObject.tabStops = self.tabStops; // copy return newObject; } |
So I overwrite the setter to substitute mutableCopy for the copy. Granted this is a very rare case to do that, but there is no other way to transfer a copy to a new object and still preserve mutability. Theoretically I could have also made the instance mutable lazily just before adding something new to it, but the code to do that is way longer than this overwritten setter and harder to understand.
Other smarter people have also stumbled over this. May this article prevent you from ever losing mutability when you depend on it.
Categories: Recipes
_birthdayDateFormatter = [_birthdayDateFormatter retain]; should be _birthdayDateFormatter = [birthdayDateFormatter retain];
Thanks for spotting that.