Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lazy Loading in Objective-C - Should I call the setter from within the getter?

This is a small detail but everytime I lazy load something I get caught up on it. Are both of these methods acceptable? Is either better? Assume that the variable has the retain property.

Method #1

(AnObject *)theObject{
    if (theObject == nil){
        theObject = [[AnObject createAnAutoreleasedObject] retain];
    }
    return theObject;
}

Method #2

(AnObject *)theObject{
    if (theObject == nil){
        self.theObject = [AnObject createAnAutoreleasedObject];
    }
    return theObject;
}

First, I'm not sure if it's OK to access another accessor function within an accessor (don't see why not, though). But it seems like setting the class variable without going through the setter could be equally bad if the setter does something special (or if the property is changed to something besides retain and the getter isn't checked).

like image 725
Jesse Anderson Avatar asked Sep 14 '10 19:09

Jesse Anderson


2 Answers

Both are actually quite fragile and not at all identical, depending on what clients of the class are doing. Making them identical is easy enough -- see below -- but making it less fragile is harder. Such is the price of lazy initialization (and why I generally try to avoid lazy initialization in this fashion, preferring to treat initialization of subsystems as a part of overall application state management).

With #1, you are avoiding the setter and, thus, anything observing the change won't see the change. By "observing", I'm specifically referring to key-value observation (including Cocoa Bindings, which uses KVO to update the UI automatically).

With #2, you will trigger the change notification, updating the UI and otherwise exactly as if the setter was called.

In both cases, you have a potential for infinite recursion if the initialization of the object calls the getter. That includes if any observer asks for the old value as a part of the change notification. Don't do that.

If you are going to use either method, consider carefully the consequences. One has the potential to leave the app in an inconsistent state because a state change of a property did not notify and the other has the potential for deadlock.

Better to avoid the issue entirely. See below.


Consider (garbage collection on, standard Cocoa command line tool:

#import <Foundation/Foundation.h>

@interface Foo : NSObject
{
    NSString *bar;
}
@property(nonatomic, retain) NSString *bar;
@end
@implementation Foo
- (NSString *) bar
{
    if (!bar) {
        NSLog(@"[%@ %@] lazy setting", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
        [self willChangeValueForKey: @"bar"];
        bar = @"lazy value";
        [self didChangeValueForKey: @"bar"];
    }
    return bar;
}

- (void) setBar: (NSString *) aString
{
    NSLog(@"[%@ %@] setting value %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), aString);
    bar = aString;
}
@end

@interface Bar:NSObject
@end
@implementation Bar
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
{
    NSLog(@"[%@ %@] %@ changed\n\tchange:%@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), keyPath, change);
}
@end

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    Foo *foo = [Foo new];
    Bar *observer = [Bar new];
    CFRetain(observer);
    [foo addObserver:observer forKeyPath:@"bar"
             options: NSKeyValueObservingOptionPrior | NSKeyValueObservingOptionNew
             context:NULL];
    foo.bar;
    foo.bar = @"baz";
    CFRelease(observer);

    [pool drain];
    return 0;
}

This does not hang. It spews:

2010-09-15 12:29:18.377 foobar[27795:903] [Foo bar] lazy setting
2010-09-15 12:29:18.396 foobar[27795:903] [Bar observeValueForKeyPath:ofObject:change:context:] bar changed
    change:{
    kind = 1;
    notificationIsPrior = 1;
}
2010-09-15 12:29:18.397 foobar[27795:903] [Bar observeValueForKeyPath:ofObject:change:context:] bar changed
    change:{
    kind = 1;
    new = "lazy value";
}
2010-09-15 12:29:18.400 foobar[27795:903] [Bar observeValueForKeyPath:ofObject:change:context:] bar changed
    change:{
    kind = 1;
    notificationIsPrior = 1;
}
2010-09-15 12:29:18.400 foobar[27795:903] [Foo setBar:] setting value baz
2010-09-15 12:29:18.401 foobar[27795:903] [Bar observeValueForKeyPath:ofObject:change:context:] bar changed
    change:{
    kind = 1;
    new = baz;
}

If you were to add NSKeyValueObservingOptionOld to the list of options for observation, it very much does hang.

Getting back to a comment I made earlier; the best solution is to not do lazy initialization as a part of your getter/setter. It is too fine grained. You are far better off managing your object graph state at a higher level and, as a part of that, have a state transition that is basically of the "Yo! I'm going to use this subsystem now! Warm that bad boy up!" that does the lazy initialization.

like image 82
bbum Avatar answered Sep 19 '22 17:09

bbum


Those methods are never identical. The first one is right, while the second one is wrong! A getter may never call will/didChangeValueForKey: and therefore also not the setter. This will lead to infinite recursion if that property is observed.

And besides, there is no state change to observe when the member is initialized. You ask your object for the theObject and you get it. When this gets created is an implementation detail and no concern to the outside world.

like image 39
Sven Avatar answered Sep 21 '22 17:09

Sven