Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Minutia on Objective-C Categories and Extensions

Tags:

objective-c

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 (readwrite, 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 (readwrite, 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?

like image 234
Matt Wilding Avatar asked Jan 13 '11 21:01

Matt Wilding


1 Answers

Class extensions were added in Objective-C 2.0 to solve two specific problems:

  1. Allow an object to have a "private" interface that is checked by the compiler.
  2. Allow publicly-readable, privately-writable properties.

Private Interface

Before Objective-C 2.0, if a developer wanted to have a set of methods in Objective-C, they often declared a "Private" category in the class's implementation file:

@interface MyClass (Private) - (id)awesomePrivateMethod; @end 

However, these private methods were often mixed into the class's @implementation block (not a separate @implementation block for the Private category). And why not? These aren't really extensions to the class; they just make up for the lack of public/private restrictions in Objective-C categories.

The problem is that Objective-C compilers assume that methods declared in a category will be implemented elsewhere, so they don't check to make sure the methods are implemented. Thus, a developer could declare awesomePrivateMethod but fail to implement it, and the compiler wouldn't warn them of the problem. That is the problem you noticed: in a category, you can declare a property (or a method) but fail to get a warning if you never actually implement it -- that's because the compiler expects it to be implemented "somewhere" (most likely, in another compilation unit independent of this one).

Enter class extensions. Methods declared in a class extension are assumed to be implemented in the main @implementation block; if they're not, the compiler will issue a warning.

Publicly-Readable, Privately-Writeable Properties

It is often beneficial to implement an immutable data structure -- that is, one in which outside code can't use a setter to modify the object's state. However, it can still be nice to have a writable property for internal use. Class extensions allow that: in the public interface, a developer can declare a property to be read-only, but then declare it to be writable in the class extension. To outside code, the property will be read-only, but a setter can be used internally.

So Why Can't I Declare a Writable Property in a Category?

Categories cannot add instance variables. A setter often requires some sort of backing storage. It was decided that allowing a category to declare a property that likely required a backing store was A Bad Thing™. Hence, a category cannot declare a writable property.

They Look Similar, But Are Different

The confusion lies in the idea that a class extension is just an "unnamed category". The syntax is similar and implies this idea; I imagine it was just chosen because it was familiar to Objective-C programmers and, in some ways, class extensions are like categories. They are alike in that both features allow you to add methods (and properties) to an existing class, but they serve different purposes and thus allow different behaviors.

like image 115
mipadi Avatar answered Oct 14 '22 12:10

mipadi