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

4

8

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).

Weatherspoon answered 14/9, 2010 at 19:37 Comment(0)
H
17

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.

Hass answered 14/9, 2010 at 19:50 Comment(4)
Actually there is no change to observe when lazily initializing the object, so this answer is not correct.Brigitte
Huh? Whether it is lazily initialized or not is irrelevant. If I set up a KVO observation of theObject and put self.theObject = ... in the getter, then that KV observation will fire. "Lazy initialization" is just a name for a pattern; neither the compiler nor the runtime know anything about it.Hass
Yes, the observation will fire and end up in infinte recursion. The setter calls willChangeValueForKey: before changing anything. willChangeValueForKey: will again call the setter, which (since the ivar still is nil) will call the setter again and so on and so on. This is the technical reason why this is wrong. But conceptually it’s also wrong to expect the setter to be called and the notification to be generated. The point of that lazy initialization is that from the point of view of outside code theObject always exists.Brigitte
Not quite. You only end up in infinite recursion if you ask for the old value in the observer.Hass
B
3

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.

Brigitte answered 15/9, 2010 at 13:1 Comment(7)
If there are observers in existence prior to the call to the getter then having the getter modify state without triggering the observation notifications will leave the application in an inconsistent state.Hass
Nope, setting up the binding will always call the getter. There is no way to get the observer into an inconsistent state.Brigitte
Ah -- yes -- I see what you are saying. Still inconsistent, though, in that the lack of a notification means that the changed state will not trigger any behaviors tied to the observation. The UI may never be inconsistent, but there may be other actions beyond UI updates that will be problematic.Hass
There is no change in state as far as the outside world is concerned. If the object is not lazily initialized, but created in the -init method no observers are notified either.Brigitte
There most definitely is a state change. Before the getter is called, theObject is not set. After the getter is called, theObject is set. A state change. Since it is lazily initialized, the encapsulating object was presumably instantiated potentially long before and is already hooked up to the surrounding object graph. Without some kind of change notification on that state change, you have the potential for inconsistency.Hass
If the object is created in the init method then, yes, there is no state change, but that is because the object has, by definition, not yet been connected to the surrounding object graph. Once instantiation is complete, all subsequent state changes are potentially of significant interest to the surrounding graph.Hass
No, that is not a state change as far as the outside world is concerned. Only that class itself can ever know that the ivar is nil. As far as anyone else is concerned this property is never nil. If other code just assumes that this has to be nil and waits for a notification that this changed it will also break when the ivar is assigned in -init. But this also doesn’t mean that creating the object in -init is also wrong. This just means that the code that depends on the assumption is doing wrong.Brigitte
L
1

If you know the property setter method is a standard retaining setter, they're identical. If not, you need to decide whether the setter's other behavior should be invoked during that operation. If you don't know, it's safest to use the setter, since its behavior may be important. Don't sweat it.

Litha answered 14/9, 2010 at 19:48 Comment(1)
It might be the same from a memory management point of view. But setters do more than just taking care of memory management, there is also KVO to think about. And that makes method #2 wrong!Brigitte
P
0

Both of those are basically identical, its really just up to you to choose which one is best for your case. You already really described the pros/cons about using the property syntax.

Plasterboard answered 14/9, 2010 at 19:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.