Tuesday, April 27, 2010

Objective-C Tuesdays: @property and @synthesize

Last time we looked at writing getters and setters for Objective-C classes. Today we'll look at generating them automatically using the @property and @synthesize directives.

Before Objective-C 2.0 was introduced, if you wanted to add getters and setters to a class, you wrote them yourself using instance methods, which caused some classes to become heavy with boilerplate code:
// example of hand-written getters and setters
@interface Person : NSObject {
  int age;
  Address *address;
  NSString *name;
}

- (int)age;
- (void)setAge:(int)anAge;

- (Address *)address;
- (void)setAddress:(Address *)anAddress;

- (NSString *)name;
- (void)setName:(NSString *)aName;

// ...

@end


@implementation Person

- (int)age {
  return age;
}

// assign-type setter
- (void)setAge:(int)anAge {
  age = anAge;
}

- (Address *)address {
  return address;
}

// retain-type setter
- (void)setAddress:(Address *)anAddress {
  if (address != anAddress) {
    [address release];
    address = [anAddress retain];
  }
}

- (NSString *)name {
  return name;
}

// copy-type setter
- (void)setName:(NSString *)aName {
  if (name != aName) {
    [name release];
    name = [aName copy];
  }
}

// ...

@end
This is a lot of code to write, and the Person class barely does anything yet. The @property directive will remove some of the boilerplate code from the @interface section of the class and the @synthesize directive will clean up the @implementation

Declaring properties
A getter and setter form a logical property of a class. Properties typically correspond directly to instance variables, but don't have to. Sometimes a property is calculated on the fly, or the name of the instance variable is different than the name of the property. The @property directive replaces the getter and setter method declarations in the @interface of the class.
// declaring age property
@interface Person : NSObject {
  int age;
  Address *address;
  NSString *name;
}

@property int age;

// ...
@end
Notice that the property declaration looks a lot like an instance variable declaration. At a high level, a property is very similar to an instance variable. But as far as the compiler cares, a @property is simply a replacement for declaring the getter and setter methods:
@property int age;
is just a substitute for
- (int)age;
- (void)setAge:(int)anAge;
If you don't write the corresponding getter and setter methods in the @implementation section, you'll see compiler warnings like this:
property 'age' requires method '-age' to be defined - use @synthesize, @dynamic or provide a method implementation
property 'age' requires the method 'setAge:' to be defined - use @synthesize, @dynamic or provide a method implementation

Read-only properties
Sometimes, you want properties to be read-only. For example, we might store the person's birthdate instead of age:
// age property calculated on the fly
@interface Person : NSObject {
  NSDate *birthDate;
  Address *address;
  NSString *name;
}

@property int age;

// ...
@end

@implementation Person

- (int)age {
  NSCalendar *calendar = [NSCalendar currentCalendar];
  NSDate *today = [NSDate date];
  NSDateComponents *components = [calendar components:NSYearCalendarUnit 
                                             fromDate:birthDate 
                                               toDate:today 
                                              options:0];
  return components.year;
}

// ...
@end
Yet it doesn't make sense to write the corresponding setAge: method here. If we compile this code, we'll still get nagged about the setter:
property 'age' requires the method 'setAge:' to be defined - use @synthesize, @dynamic or provide a method implementation
To silence this, we need to add an attribute to the property. Property attributes go in parentheses after the @property keyword but before the type and name:
@property (readonly) int age;
Here we've told the compiler that the age property is readonly, so not to worry about the setter. By default, properties are readwrite. You can label them with the readwrite attribute, but since it's the default, it's redundant and you'll rarely see it.

Object properties
Let's set aside the calculated age property and go with our plain old int version. The next property of the Person class is address:
// declaring address property
@interface Person : NSObject {
  int age;
  Address *address;
  NSString *name;
}

@property int age;
@property Address *address;

// ...
@end
When you compile this, you'll see warnings like:
no 'assign', 'retain', or 'copy' attribute is specified - 'assign' is assumed
assign attribute (default) not appropriate for non-gc object property 'address'
The compiler has noticed that address is an Objective-C object type and is reminding you to do the appropriate memory management. (Those lucky Mac developers don't have to worry about this any more since they now have garbage collection.) In addition to readwrite/readonly there's another set of property attributes: assign, retain and copy. Since address uses retain memory management, we'll change it to look like
@property (retain) Address *address;
At this stage, assign, retain and copy are simply documentation to other programmers about the memory management strategy for the property. If you write the setter yourself, the compiler isn't smart enough to tell if you actually wrote the correct type of setter, so be careful! There's nothing worse than code that says it's doing one thing and actually does another. So what's the point? We'll see when we get to @synthesize.

Finishing up the properties for our class, we declare a property for name that uses a copy memory management scheme. (Last week's post explained why we use copy for NSString properties.)
// declaring name property
@interface Person : NSObject {
  int age;
  Address *address;
  NSString *name;
}

@property int age;
@property (retain) Address *address;
@property (copy) NSString *name;

// ...
@end

Plain old pointer properties
Most Objective-C code uses objects instead of plain old C structs, strings and arrays, but sometimes you'll need to use them, often when working with low level C libraries. You might be tempted to document your memory management for these plain old pointers as you would Objective-C objects:
// ERROR: won't compile
@interface Person : NSObject {
  int age;
  Address *address;
  NSString *name;
  char const *username; // plain old C string
}

@property int age;
@property (retain) Address *address;
@property (copy) NSString *name;
@property (copy) char const *username; // plain old C string

// ...
@end
Unfortunately, since plain old C types don't have retain/release memory management like Objective-C objects, this muddies the meaning of copy and the compiler will stop with an error like:
property 'username' with 'copy' attribute must be of object type
Unfortunately, marking a property like this assign when your setter actually makes a copy doesn't seem right either:
// not quite right
@property (assign) char const *username;
My advice is to leave off assign, retain and copy from properties for plain old C pointers and use a comment to note your memory management strategy.

I'm out of time right now, so I'll have to finish this up next week. Coming up, the nonatomic property attribute, and the @synthesize directive.

3 comments:

Chris Ryland said...

Something I've noticed is that you can leave out the initial declaration of a property in the class "struct" and have only the @property declaration (which seems like a good thing for DRY).

Don McCaughey said...

I've not been able to do this on iPhone OS; I think it only applies to recent versions of Objective-C on OS X. And I agree that having to declare both the instance variable and the property violates the DRY principle.

Tristan said...

@Chris Ryland You're right, you can leave it out. The reason you'd want to keep it is that if you leave it out, the member variable is generated for you, you don't get access to it when you're debugging, which is a pain.