Item 26: Avoid Properties in Categories
A property is a way of encapsulating data (see Item 6). Although it is technically possible to declare a property in a category, you should avoid doing so if possible. The reason is that it is impossible for a category, except the class-continuation category (see Item 27), to add instance variables to a class.Therefore, it is also impossible for the category to synthesize an instance variable to back the property.
Suppose that after reading Item 24, you have decided to use categories to split up into distinct fragments your implementation of a class representing a person. You might decide to have a friendship category for all the methods relating to manipulating the list of friends associated with a person. Without knowing about the problem described earlier, you may also put the property for the list of friends within the friendship category, like so:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
@end
@implementation EOCPerson
// Methods
@end
@interface EOCPerson (Friendship)
@property (nonatomic, strong) NSArray *friends;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
@implementation EOCPerson (Friendship)
// Methods
@end
If you compile this, however, you would end up with compiler warnings:
warning: property 'friends' requires method 'friends' to be
defined - use @dynamic or provide a method implementation in
this category [-Wobjc-property-implementation]
warning: property 'friends' requires method 'setFriends:' to be
defined - use @dynamic or provide a method implementation in
this category [-Wobjc-property-implementation]
This slightly cryptic warning means that instance variables cannot be synthesized by a category and, therefore, that the property needs to have the accessor methods implemented in the category.Alternatively, the accessor methods can be declared @dynamic,
meaning that you are declaring that they will be available at runtime but cannot be seen by the compiler. This might be the case if you are using the message-forwarding mechanism (see Item 12) to intercept the methods and provide the implementation at runtime.
To get around the problem of categories not being able to synthesize instance variables, you can useassociated objects (see Item 10). For the example, you would need to implement the accessors within the category as follows:
#import <objc/runtime.h>
static const char *kFriendsPropertyKey = "kFriendsPropertyKey";
@implementation EOCPerson (Friendship)
- (NSArray*)friends {
return objc_getAssociatedObject(self, kFriendsPropertyKey);
}
- (void)setFriends:(NSArray*)friends {
objc_setAssociatedObject(self,
kFriendsPropertyKey,
friends,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
This works, but it’s less than ideal. It’s a fair amount of boilerplate and is prone to errors with memory management because it’s easy to forget that the property is implemented like this. For example, you may change the memory-management semantics by changing the property attributes. However, you would also need to remember to change the memory-management semantics of the associated object in the setter. So although this is not a bad solution, it is not one that I recommend.
Also, you may want the instance variable backing the friends
array to be a mutable array. You could take a mutable copy, but it’s yet another vector for confusion to enter your code base. It is therefore much cleaner to define properties in the main interface definition rather than to define them in categories.
In this example, the correct solution is to keep all the property definitions in the main interface declaration. All the data encapsulated by a class should be defined in the main interface, the only place where instance variables (the data) can be defined. Since they are just syntactic sugar for defining an instance variable and associated accessor methods, properties fall under the same rule. Categories should be thought of as a way to extend functionality of the class, not the data that it encapsulates.
That being said, sometimes read-only properties can be successfully used within categories. For example, you might want to create a category on NSCalendar
to return an array containing the months as strings. Since the method is not accessing any data and the property is not backed by an instance variable, you could implement the category like this:
@interface NSCalendar (EOC_Additions)
@property (nonatomic, strong, readonly) NSArray *eoc_allMonths;
@end
@implementation NSCalendar (EOC_Additions)
- (NSArray*)eoc_allMonths {
if ([self.calendarIdentifier
isEqualToString:NSGregorianCalendar])
{
return @[@"January", @"February",
@"March", @"April",
@"May", @"June",
@"July", @"August",
@"September", @"October",
@"November", @"December"];
} else if ( /* other calendar identifiers */ ) {
/* return months for other calendars */
}
}
@end
Autosynthesis of an instance variable to back the property will not kick in, because all the required methods (only one in this read-only case) have been implemented. Therefore, no warnings will be emitted by the compiler. Even in this situation, however, it is generally better to avoid using a property.A property is meant to be backed by data held by the class. A property is for encapsulating data. In this example, you would instead declare the method to retrieve the list of months within the category:
@interface NSCalendar (EOC_Additions)
- (NSArray*)eoc_allMonths;
@end
Things to Remember
Keep all property declarations for encapsulated data in the main interface definition.
Prefer accessor methods to property declarations in categories, unless it is a class-continuation category.