Lazy Properties Dynamic Implementation
November 16, 2011

In my previous post I showed a relatively clean way to implement lazy properties generically using the dynamic runtime.

So how does this dynamic implementation actually work? The interface is pretty small:

@interface NSObject(ECLazyProperties)

+ (void)initializeLazyProperties;

#define lazy_synthesize(name,init) \
class Dummy__; \
- (id)name##Init__ \
{ \
id value = [self name##Init__]; \
if (!value) \
{ \
    value = (init); \
    [self setValue:value forKey:@#name]; \
} \
return value; \
}

#define lazy_synthesize_method(name,method) lazy_synthesize_value(name,[self method])

@end

Most of the work here is in one macro.

Essentially what this does is define a lazy initialisation method for a property. The method:

We define the name of the method by appending Init__ to the property name. We could have appended just Init, but this method really shouldn’t be called publicly, so I went for a slightly more obscure name.

How it calls the original getter is kind of interesting. If you unpick the macro you’ll see that it appears to be calling itself. Isn’t this going to lead to some hideous recursion?

The answer is no - because the method will have been swizzled. When we’re actually running in the method, it will be as a result of a call to the original getter method. So if we want to call the original getter method, we need to actually call the init method instead!

If we do discover that the value is uninitialised, we then use KVC to call the proper setter with an initial value.

So all of this relies on some swizzling. How do we set that up?

The implementation of initializeLazyProperties looks like this:

+ (void)initializeLazyProperties
{
    uint count;
    objc_property_t* properties = class_copyPropertyList(self, &count);
    for (int i = 0; i < count ; i++)
    {
        const char* propertyName = property_getName(properties[i]);
        SEL init = NSSelectorFromString([NSString stringWithFormat:@"%sInit__", propertyName]);
        if ([self instancesRespondToSelector:init])
        {
            SEL getter = NSSelectorFromString([NSString stringWithFormat:@"%s", propertyName]);
            Method initMethod = class_getInstanceMethod(self, init);
            Method getterMethod = class_getInstanceMethod(self, getter);
            
            method_exchangeImplementations(initMethod, getterMethod);
        }
    }
    free(properties);
}

We iterate through a list of properties, looking to see if there is a corresponding lazy initialise method for it. If there is, we simply swizzle the original getter method with the init method.

When the user calls the getter, they’ll actually get the init method. This will call on to the getter, check the value, and initialise if necessary.

Complex Initialisation

In the example, we’re just supplying a fixed initial value, but of course the real point of lazy initialisation is for situations where we need to do something time consuming in the initialisation, and we only want to actually do it if we have to.

For this situation you can use the second macro - lazy_synthesize_method. Instead of passing in an initial value, you pass in a method to call to return the value.

Conclusions

This code seems to actually work, but I’ve not tested it intensively to see whether it behaves property in all situations - for example with KVC/KVO.

I think it should do, but right now I view it largely as a thought experiment.

Right back at the beginning I said that the way I really think it should work is

@property (nonatomic, retain, lazy) NSString* name;

I still think that would be ideal. I think that what I’ve done illustrates that it ought to be entirely possible to add a hack to Clang to implement this, but I leave that as an exercise for the reader…

If anyone is interested in the full source code to my implementation, you’ll find it in the ECCore module of my open source ECFoundation library.

[Updated Feb 2012 to fix some broken links]

« ECLogging - Better Logging For Objective-C Lazy Properties Revisited (Again) »
Got a comment on this post? Let us know at @elegantchaoscom.