8

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

方法#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做了一些特殊的事情(或者如果属性更改为retain之外的东西并且没有检查getter),似乎在不通过setter的情况下设置类变量可能同样糟糕。

4

4 回答 4

17

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

使用#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 的一部分。它的粒度太细了。你最好在更高级别管理你的对象图状态,作为其中的一部分,状态转换基本上是“哟!我现在要使用这个子系统!让那个坏孩子暖和起来! " 进行延迟初始化。

于 2010-09-14T19:50:58.930 回答
3

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

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

于 2010-09-15T13:01:31.623 回答
1

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

于 2010-09-14T19:48:05.963 回答
0

这两者基本上是相同的,它真的只是由你来选择哪一个最适合你的情况。您已经真正描述了使用属性语法的优缺点。

于 2010-09-14T19:44:16.373 回答