Minutia on Objective-C Categories and Extensions.
- by Matt Wilding
I learned something new while trying to figure out why my readwrite property declared in a private Category wasn't generating a setter. It was because my Category was named:
// .m
@interface MyClass (private)
@property (readwrite, copy) NSArray* myProperty;
@end
Changing it to:
// .m
@interface MyClass ()
@property (readwrite, copy) NSArray* myProperty;
@end
and my setter is synthesized. I now know that Class Extension is not just another name for an anonymous Category. Leaving a Category unnamed causes it to morph into a different beast: one that now gives compile-time method implementation enforcement and allows you to add ivars. I now understand the general philosophies underlying each of these: Categories are generally used to add methods to any class at runtime, and Class Extensions are generally used to enforce private API implementation and add ivars. I accept this.
But there are trifles that confuse me. First, at a hight level: Why differentiate like this? These concepts seem like similar ideas that can't decide if they are the same, or different concepts. If they are the same, I would expect the exact same things to be possible using a Category with no name as is with a named Category (which they are not). If they are different, (which they are) I would expect a greater syntactical disparity between the two. It seems odd to say, "Oh, by the way, to implement a Class Extension, just write a Category, but leave out the name. It magically changes."
Second, on the topic of compile time enforcement: If you can't add properties in a named Category, why does doing so convince the compiler that you did just that? To clarify, I'll illustrate with my example. I can declare a readonly property in the header file:
// .h
@interface MyClass : NSObject
@property (readonly, copy) NSString* myString;
@end
Now, I want to head over to the implementation file and give myself private readwrite access to the property. If I do it correctly:
// .m
@interface MyClass ()
@property (readonly, copy) NSString* myString;
@end
I get a warning when I don't synthesize, and when I do, I can set the property and everything is peachy. But, frustratingly, if I happen to be slightly misguided about the difference between Category and Class Extension and I try:
// .m
@interface MyClass (private)
@property (readonly, copy) NSString* myString;
@end
The compiler is completely pacified into thinking that the property is readwrite. I get no warning, and not even the nice compile error "Object cannot be set - either readonly property or no setter found" upon setting myString that I would had I not declared the readwrite property in the Category. I just get the "Does not respond to selector" exception at runtime. If adding ivars and properties is not supported by (named) Categories, is it too much to ask that the compiler play by the same rules? Am I missing some grand design philosophy?