See Also
Variables in Objective-C
Last time we looked at instance variables of Objective-C classes. Today we're going to look at writing getters and setters for instance variables.Variables in Objective-C
As we saw, instance variables have protected scope by default, which makes them visible in instance methods of the class that defines them and all subclasses, but hidden to all other code and classes by default. Hiding the inner workings of a class is a principal of object oriented programming known as encapsulation. In general, it's better to tell your objects what to do rather than ask them for their data. But there are times when you need to get and set an object's data directly, particularly for objects that correspond to those things your application and users directly manipulate, like address book contacts or cookie recipes. The typical way to do this is to write getter and setter methods. Before Objective-C 2.0, these were always written by hand, and there are still times when you would want to write your own rather have the compiler generate them for you using the
@property
and @synthesize
directives.Getters
A conventional getter method in Objective-C has the same name as the instance variable:
// getter method example @interface CookieRecipe : NSObject { NSString *name; // instance variable name } - (NSString *)name; // getter method for name ivar @end @implementation CookieRecipe - (NSString *)name { // getter method for name ivar return name; } @endThis works because instance variables and instance methods live in separate namespaces. The compiler can always figure out whether you're calling a method or accessing an instance variable, so there's never a problem. Getter methods are generally very simple and rarely have side effects.
Compiler generated getters are as simple as the example above. There are two common cases where you might want to write them by hand: calculated properties and defensive copying.
Sometimes a class has a property that's easily calculated from other properties, such as a
fullName
property that can be created on the fly by joining firstName
and lastName
, or a yearsWithCompany
property that's calculated by subtracting hireDate
from today's date. These types of properties are usually read only and quick to calculate.Defensive copying is done when your object has some mutable internal data that it needs to share with other code, but which other code should not change. For example, you might have a
User
class that has an NSMutableArray
containing the user's friends
:// example of exposing mutable internals @interface User : NSObject { NSMutableArray *friends; // contains other Users } - (NSMutableArray *)friends; @end @implementation User - (NSMutableArray *)friends { return friends; } @endThe
friends
getter returns the friends
instance variable directly. This might work out okay if all the code that uses friends
is polite and only reads the list of friends. But it's all to easy to innocently do something like this:// accidentally changing internal state NSMutableArray *boneheads = [user friends]; // boneheads now points to same mutable array that // the friends instance variable does NSPredicate *boneheadsOnly = [NSPredicate predicateWithFormat:@"species == 'Minbari'"]; [boneheads filterUsingPredicate: boneheadsOnly]; // oops! all the user's non-Minbari friends are gone // -filterUsingPredicate: removes items that don't matchMutable objects like
NSMutableArray
naturally have methods like -filterUsingPredicate:
that change the data they contain. By sharing a mutable instance variable directly, the User
class allows other code to intentionally or unintentionally muck around with its internal state, breaking encapsulation. While bugs caused by unintentional mucking aren't usually too hard to track down, it's the intentional mucking that causes more trouble in the long run. By exposing its internals like this, the User
class allows itself to become closely coupled with the code that calls it, since callers can add or remove items directly to or from the friends
collection. The rules for adding and removing friends get spread around the app in numerous locations rather than centralized in User
, making the system harder to understand, harder to change; small changes in User
then affect more of the code than necessary.So rather than return a mutable internal value directly, there are a couple of options in Objective-C. When you're using a class like
NSMutableArray
that has an immutable superclass, you can upcast the returned instance variable to the immutable superclass:// getter that upcasts to immutable superclass @interface User : NSObject { NSMutableArray *friends; // instance variable is mutable } - (NSArray *)friends; @end @implementation User // return type of getter is immutable - (NSArray *)friends { return friends; } @endIt's called upcasting since it casts a subclass reference "up" to a superclass reference. Because this is always a safe cast, Objective-C will do this automatically for you with no explicit cast is needed. While this won't prevent a malicious programmer from downcasting the result back to its mutable type and mucking around, in practice this works pretty well (and if you have a malicious programmer on your team, you have a bigger problem than a little code can solve).
Sometimes the instance variable you want to share doesn't have an immutable superclass, or most callers of the getter need to filter or manipulate the returned value. In that case, you can do an actual defensive copy:
// getter that does a defensive copy @interface User : NSObject { NSMutableArray *friends; // instance variable is mutable } - (NSMutableArray *)friends; @end @implementation User // return type of getter is mutable - (NSMutableArray *)friends { // return an autoreleased copy return [[friends mutableCopy] autorelease]; } @endNow the caller can delete your friends all they want. You can use whichever creation or copy method is appropriate for the mutable data; just make sure to properly
autorelease
the return value.Setters
The Objective-C convention for setter names is similar to Java's: capitalize the instance variable name and prefix with
set
. Setters come in different varieties, depending on the type of instance variable. The simplest kind is used for primitive types like int
s and object references that aren't retained, like delegates.// assignment setter - (void)setAge:(int)anAge { age = anAge; }Assignment setters are trivial, but the fun starts when you have to deal with memory management and retain counts. Here's an example of a setter that retains the new value and releases the old one, but it has a subtle bug:
// retain setter WITH BUG - (void)setAddress:(Address *)theAddress { [address release]; // release old address address = [theAddress retain]; // retain new address }This setter might never give you a problem, if you're lucky. But what happens if the new address and old address are the same object? What happens when that object's retain count is 1?
// retain setter WITH BUG // suppose address == theAddress // and retain count is 1 - (void)setAddress:(Address *)theAddress { // retain count is 1 [address release]; // retain count now 0, // dealloc called address = [theAddress retain]; // oops! theAddress points at // invalid memory }There are two common ways to write a setter to get around this. The first way is to check for self-assignment:
// retain setter with self-assignment check - (void)setAddress:(Address *)theAddress { if (theAddress != address) { [address release]; // release old address address = [theAddress retain]; // retain new address } }The second way is to retain first and release last:
// retain setter that retains first - (void)setAddress:(Address *)theAddress { [theAddress retain]; // retain new address [address release]; // release old address address = theAddress; }This will prevent the retain count from going to zero in the case of self-assignment.
Just as you may want to defensively copy in a getter, you may want to do the same in a setter. Here's an example a malicious programmer would love:
// be careful with that setter @interface User : NSObject { NSString *name; } - (void)setName:(NSString *)aName; @end @implementation User - (void)setName:(NSString *)aName { if (aName != name) { [name release]; name = [aName retain]; } } @endLooks normal, right? But what if I do this:
// don't change that mutable object! NSMutableString *someName = [@"Joe User" mutableCopy]; User user = // ... [user setName: someName]; // okay cool, user's name is now "Joe User" [someName appendString:@" Doodoo Head"]; // oops, user's name and someName point to the same object // which now contains "Joe User Doodoo Head"So even though you thought you were using an immutable
NSString
for your user's name, many common Cocoa Touch classes like NSString
and NSArray
have mutable subclasses, and your object's callers can accidentally give you a mutable instance that they modify later. So when you're writing setters in this situation, you should defensively copy the value you receive.// setter that defensively copies - (void)setName:(NSString *)aName { if (aName != name) { [name release]; name = [aName copy]; } }You can use the
-copy
method if the object implements the NSCopying
protocol, or any creation method that produces a new independent object. (Just make sure to retain that copy if appropriate).Next time, we'll look at how the
@property
and @synthesize
directives are used in modern Objective-C to generate getters and setters automatically.
7 comments:
Let me tell you quality has definitely gone up from the first posts. I added your blog to my RSS feed when I found them a few months ago, eagerly waiting for you to continue writing, and I am not disappointed.
I will continue to follow your posts, since I learned ObjC in a very messy fashion, and I'm sure your articles will help me get the basic concepts straight.
They'll also serve beginners, when they land here by googling for some keywords. ;)
Thanks Mike! I'm glad you're finding them useful and I'll endeavor to keep improving.
When declaring getter for NSMutableArray you forgot an asterisk operator:
- (NSMutableArray *)friends;
instead of
- (NSMutableArray)friends;
Thanks for the catch Eimantas.
@Eimantas Thanks for the proofreading -- I fixed the errors.
I agree with Mike, it just get's better and better. I am familiar with most of the stuff, but always find myself leaving these articles with an even better understanding of the basics, and often a new trick or two:)
There are tons of books and blogs out there whose tutorials loose their clarity because they must always deliver a working, compiling example. It is nice to see someone focusing on the language instead of the framework.
Bet you get a lot out of writing these too:)
Thanks Ricki! Yeah, they're fun to write. Trying to explain something really helps me get things straight in my own head, and when I dig into a topic, I often learn something new about it.
Post a Comment