【问题标题】:Lazy Loading in Objective-C - Should I call the setter from within the getter?Objective-C 中的延迟加载 - 我应该从 getter 中调用 setter 吗?
【发布时间】:2010-09-14 19:37:17
【问题描述】:

这是一个小细节,但每次我懒加载某些东西时,我都会陷入困境。这两种方法都可以接受吗?哪个更好?假设变量具有retain属性。

方法#1

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

方法#2

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

首先,我不确定在访问器中访问另一个访问器函数是否可以(但不明白为什么不这样做)。但是,如果 setter 做了一些特殊的事情(或者如果属性被更改为除了保留之外的东西并且没有检查 getter),那么在不通过 setter 的情况下设置类变量似乎同样糟糕。

【问题讨论】:

    标签: objective-c memory-management lazy-loading lazy-initialization


    【解决方案1】:

    两者实际上都非常脆弱,而且完全不一样,这取决于班级的客户在做什么。使它们相同很容易 - 见下文 - 但使其不那么脆弱更难。这就是延迟初始化的代价(以及为什么我通常会尽量避免以这种方式进行延迟初始化,更愿意将子系统的初始化视为整个应用程序状态管理的一部分)。

    使用 #1,您可以避免使用 setter,因此,任何观察到变化的东西都不会看到变化。这里所说的“观察”,我特指的是键值观察(包括 Cocoa Bindings,它使用 KVO 自动更新 UI)。

    使用 #2,您将触发更改通知、更新 UI 等,就像调用了 setter 一样。

    在这两种情况下,如果对象的初始化调用 getter,则可能会无限递归。这包括是否有任何观察者要求将旧值作为更改通知的一部分。不要那样做。

    如果您打算使用任何一种方法,请仔细考虑后果。一个有可能使应用程序处于不一致的状态,因为属性的状态更改没有通知,另一个有可能出现死锁。

    最好完全避免这个问题。见下文。


    考虑(垃圾收集,标准 Cocoa 命令行工具:

    #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;
    }
    

    这不会挂起。它喷出:

    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;
    }
    

    如果您要将NSKeyValueObservingOptionOld 添加到观察选项列表中,它确实会挂起。

    回到我之前发表的评论;最好的解决方案是不要将延迟初始化作为 getter/setter 的一部分。它的粒度太细了。你最好在更高级别管理你的对象图状态,作为其中的一部分,有一个状态转换,基本上是“哟!我现在要使用这个子系统!让那个坏男孩暖和起来! "进行延迟初始化。

    【讨论】:

    • 其实懒惰初始化对象时观察不到变化,所以这个答案是正确的。
    • 嗯?是否延迟初始化无关紧要。如果我设置theObject 的KVO 观察并将self.theObject = ... 放入getter,那么KV 观察将触发。 “延迟初始化”只是一种模式的名称;编译器和运行时都不知道它。
    • 是的,观察将触发并以无限递归结束。 setter 在更改任何内容之前调用willChangeValueForKey:willChangeValueForKey: 将再次调用 setter,它(因为 ivar 仍然为 nil)将再次调用 setter,依此类推。这是错误的技术原因。但是在概念上期望调用setter并生成通知也是错误的。延迟初始化的要点是,从外部代码的角度来看theObject总是存在。
    • 不完全。如果您要求观察者中的旧值,您只会以无限递归结束。
    【解决方案2】:

    这些方法从不完全相同。第一个是正确的,而第二个是错误! getter 可能永远不会调用will/didChangeValueForKey:,因此也不会调用 setter。如果观察到该属性,这将导致无限递归。

    此外,当成员初始化时,没有观察到状态变化。你向你的对象询问theObject,你就得到了。创建它的时间是一个实现细节,与外界无关。

    【讨论】:

    • 如果在调用 getter 之前存在观察者,那么让 getter 修改状态而不触发观察通知将使应用程序处于不一致状态。
    • 不,设置绑定将始终调用 getter。没有办法让观察者进入不一致的状态。
    • 啊——是的——我明白你在说什么。但是,仍然不一致,因为缺少通知意味着更改的状态不会触发与观察相关的任何行为。 UI 可能永远不会不一致,但除了 UI 更新之外,可能还有其他操作会出现问题。
    • 状态没有变化就外界而言。如果对象没有延迟初始化,而是在-init 方法中创建,则也不会通知观察者。
    • 不,就外界而言,这不是状态变化。只有该类本身才能知道 ivar 是 nil。就其他人而言,此属性从不 nil。如果其他代码只是假设它必须是nil 并等待通知它改变了它也会在-init 中分配 ivar 时中断。但这也不意味着在-init 中创建对象也是错误的。这只是意味着依赖于假设的代码做错了。
    【解决方案3】:

    如果您知道属性设置器方法是标准的保留设置器,那么它们是相同的。如果没有,您需要决定是否应该在该操作期间调用 setter 的其他行为。如果您不知道,使用 setter 是最安全的,因为它的行为可能很重要。不要出汗。

    【讨论】:

    • 从内存管理的角度来看可能是一样的。但是 setter 不仅仅是处理内存管理,还有 KVO 需要考虑。这使得方法#2 错了!
    【解决方案4】:

    这两者基本上是相同的,这完全取决于您选择哪个最适合您的情况。您已经真正描述了使用属性语法的优缺点。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-08-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-27
      相关资源
      最近更新 更多