Better way than write dozens of empty getters?

94 views Asked by At

I use lazy instantiation on my properties, to have my class created and used as fast as possible. To achieve this, I write lots of 'empty' getters like this:

- (VMPlacesListFilter *)currentFilter
{
    if (!_currentFilter) {
        _currentFilter = [[VMPlacesListFilter alloc] init];
    }

    return _currentFilter;
}

They are all the same: if the instance variable is nil, call the -alloc and -init on the class of the property, then return the instance variable. Very common and straightforward.

If I don't create this getter by myself, Objective-C's automatic synthesization creates a getter for me, which does only the returning part (does not init the object if the instance variable is nil).

Is there any way to avoid writing this boilerplate code?

3

There are 3 answers

0
johnpatrickmorgan On

If it's the verboseness that bothers you, I suppose you could compress lazy initialisers that only need one-line initialization using the ternary operator:

- (VMPlacesListFilter *)currentFilter
{
    return _currentFilter ? : (_currentFilter = [[VMPlacesListFilter alloc] init]);
}

DISCLAIMER: I don't do this, but it's interesting that it can be done

0
Renfei Song On

First off, I totally agree with @zpasternack that "lazy load" should not be misused. However, automatically generating setters and getters is completely doable with the power of Objective-C runtime. In fact, CoreData is doing this.

Anyway, I have come up with some stupid code implementing a class called LazyClass, in which you can declare dynamic properties like lazyArray (see below). Using dynamic method resolution, when the property is accessed for the first time, a getter that calls the corresponding class's default +alloc and -init method will be automatically added to the class. All underlying instance variables are stored in an NSMutableDictionary called myVars. Of course you can manipulate ivars through the runtime API as well, but using a dictionary should save some work.

Please note that this implementation just shows the basic idea of how it works. It lacks error checking and is not supposed to be shipped.

LazyClass.h

@interface LazyClass : NSObject

@property NSMutableDictionary *myVars;

// lazily initialized property
@property NSArray *lazyArray;

@end

LazyClass.m

#import "LazyClass.h"
#import <objc/objc-runtime.h>

@implementation LazyClass

@dynamic lazyArray;

- (instancetype)init {
    self = [super init];

    self.myVars = [NSMutableDictionary dictionary];

    return self;
}

- (NSMutableDictionary *)getMyVars {
    return self.myVars;
}

// the generated getter method
id dynamicGetterMethodIMP(id self, SEL _cmd) {
    // selector name, which is also the property name
    const char *selName = sel_getName(_cmd);
    NSString *selNSName = [NSString stringWithCString:selName encoding:NSUTF8StringEncoding];

    NSString *keyPath = [NSString stringWithFormat:@"myVars.%@", selNSName];
    if (![self valueForKeyPath:keyPath]) {
        // get the actual type of the property
        objc_property_t property = class_getProperty([self class], selName);
        const char *attr = property_getAttributes(property);
        NSString *attrString = [[NSString alloc] initWithCString:attr encoding:NSUTF8StringEncoding];
        NSString *typeAttr = [[attrString componentsSeparatedByString:@","] firstObject];
        NSString *typeName = [typeAttr substringWithRange:NSMakeRange(3, typeAttr.length - 4)];

        // the default initialization
        Class typeClass = NSClassFromString(typeName);
        [self setValue:[[typeClass alloc] init] forKeyPath:keyPath];
    }

    return [self valueForKeyPath:keyPath];
}

// the generated setter method
void dynamicSetterMethodIMP(id self, SEL _cmd, id value) {
    // get the property name out of selector name
    // e.g. setLazyArray: -> lazyArray
    NSString *propertyName = NSStringFromSelector(_cmd);
    propertyName = [propertyName stringByReplacingOccurrencesOfString:@"set" withString:@""];
    propertyName = [propertyName stringByReplacingOccurrencesOfString:@":" withString:@""];
    propertyName = [NSString stringWithFormat:@"%@%@", [propertyName substringToIndex:1].lowercaseString, [propertyName substringFromIndex:1]];

    NSString *keyPath = [NSString stringWithFormat:@"myVars.%@", propertyName];
    [self setValue:value forKeyPath:keyPath];
}

// dynamic method resolution
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    if ([NSStringFromSelector(aSEL) containsString:@"set"]) {
        class_addMethod([self class], aSEL, (IMP)dynamicSetterMethodIMP, "^?");
    } else {
        class_addMethod([self class], aSEL, (IMP)dynamicGetterMethodIMP, "v@:");
    }

    return YES;
}

@end

Documentation

1
zpasternack On

Nope, I'm afraid there's no good way around it, if you really want to have lazy initialization. Personally, I usually save lazy initialization for stuff that could really be time consuming or memory intensive (say, loading images or view controllers), and initialize cheap stuff (like simple data structures or model objects) in init.

- (instancetype) init {
    self = [super init];
    if( self ) {
        _cheapThing1 = [NSMutableArray array];
        _cheapThing2 = [[MyModelObject alloc] init];
    }
    return self;
}

- (ExpensiveThing*) expensiveThing
{
    if( _expensiveThing == nil ) {
        _expensiveThing = [[ExpensiveThing alloc] init];
    }
    return _expensiveThing;
}

Unless you're loading something from disk or the network, I wouldn't worry too much about initialization time. Of course, profile it.

I know this is an Objective-C question, but it's worth noting that Swift has lazy initialization built-in.

lazy var currentFilter = VMPlacesListFilter()