何时覆盖Objective-C getters

3

在过去的一年中,我第一次与其他人合作开展了一些Objective-C项目。

偶尔(并且越来越频繁),我看到其他人重写getter / accessor方法,并在该方法中包含实现代码!对我来说,这很疯狂,因为这就是拥有setter的整个意义...这也意味着在getter中覆盖设置的属性将毫无意义。

这些人行为不当吗?还是我错过了什么?是否有必要覆盖合成属性的getter方法?

示例:

@synthesize width;

- (CGFloat)width {
  NSNumber *userWidth = [[NSUserDefaults standardUserDefaults] objectForKey:@"USER_WIDTH"];

  if (userWidth != nil) 
  {
    width = [NSNumber floatValue];
  }     

  //Shouldn't the above lines be done in the SETTER? I have SET the property!
  //Why would I ever need to write anything BUT the below line??       
  return width;
}

- (void)setWidth:(CGFloat)newWidth {
  //THIS is where you do the the setting!
  width = newWidth;
}

更新:

好的,宽度是一个糟糕的例子。太多人陷入了“变量是什么”和“不要在Objective-C访问器中包含get”的语义上。因此,我已经更新了上面的例子,忽略了不相关的语义,集中于概念。这个概念是...是否有任何例子需要重写GETTER(不是setter,只有getter。我很多次重写了setter,这个问题是关于getter的)?

返回另一个属性,比如图层(如下所述),是一个真正的例子。但更具体地说,是否有必要在GETTERSET属性?这是我看到的一些奇怪之处,所以我更新了上面的getter,从NSUserDefaults中提取一个值来证明我的观点...


你能否在头文件中添加宽度的定义? - bryanmac
5个回答

12

首先,Cocoa的命名约定会将getter命名为-width,而不是-getWidth。 "Get"一词用于填充传递的参数:

- (void) getWidth:(CGFloat *)outWidth
{
    if (outWidth) *outWidth = _width;
}

话虽如此,回到您最初的问题:

@property@synthesize之前的旧时代,我们必须手动编写访问器,就像您上面所做的那样。

然而,还有其他情况下需要手动编写访问器。

其中一个常见的情况是延迟初始化直到需要值。例如,假设有一张需要较长时间生成的图像。每次修改会改变该图像的属性时,您不希望立即重新绘制该图像。相反,您可以推迟绘制,直到下一次有人要求绘制时再进行绘制:

- (UIImage *) imageThatTakesAwhileToGenerate
{
    if (!_imageThatTakesAwhileToGenerate) {
        // set it here
    } 

    return _imageThatTakesAwhileToGenerate;
} 


- (void) setColorOfImage:(UIColor *)color
{
    if (_color != color) {
        [_color release];
        _color = [color retain];

        // Invalidate _imageThatTakesAwhileToGenerate, we will recreate it the next time that the accessor is called
        [_imageThatTakesAwhileToGenerate release];
        _imageThatTakesAwhileToGenerate = nil;
    }
}

另一个用途是将访问器/修改器的实现转发到另一个类。例如,UIView 将其许多属性转发到后备的 CALayer:

// Not actual UIKit implementation, but similar:
- (CGRect) bounds { return [[self layer] bounds]; }
- (void) setBounds:(CGRect)bounds { [[self layer] setBounds:bounds]; }
- (void) setHidden:(BOOL)hidden { [[self layer] setHidden:hidden]; }
- (BOOL) isHidden { return [[self layer] isHidden]; }
- (void) setClipsToBounds:(BOOL)clipsToBounds { [[self layer] setMasksToBounds:clipsToBounds]; }
- (BOOL) clipsToBounds { return [[self layer] masksToBounds]; }

更新问者的最新信息:

从你的更新看来,这段代码要么试图使用NSUserDefaults持久化width的值,要么试图允许用户指定一个自定义值来覆盖所有返回的宽度。如果是后者,你的示例代码是可以的(尽管我会限制这种做法,因为它可能会使新手感到困惑)。

如果是前者,你需要在setter方法中从NSUserDefaults中加载一次该值,并将新值保存回NSUserDefaults。例如:

static NSString * const sUserWidthKey = @"USER_WIDTH";

@implementation Foo {
    CGFloat _width;
    BOOL _isWidthIvarTheSameAsTruthValue;
}

@synthesize width = _width;

- (CGFloat) width
{
    if (!_isWidthIvarTheSameAsTruthValue) {
        NSNumber *userWidth = [[NSUserDefaults standardUserDefaults] objectForKey:sUserWidthKey];
        if (userWidth != nil) _width = [NSNumber doubleValue];
        _isWidthIvarTheSameAsTruthValue = YES;
    }

    return _width;
}

- (void) setWidth:(CGFloat)newWidth
{
    if (_width != newWidth) {
        _width = newWidth;
        NSNumber *userWidthNumber = [NSNumber numberWithDouble:_width];
        [[NSUserDefaults standardUserDefaults] setObject:userWidthNumber forKey:sUserWidthKey];
        _isWidthIvarTheSameAsTruthValue = YES;
    }
}

@end

_width实例变量被用作缓存。事实上,真实数据存储在NSUserDefaults中。

注意:我在这个示例中使用NSUserDefaults,因为你在你的示例中也使用了它。但在实际应用中,我更喜欢不将NSUserDefaults与我的访问器混合使用 ;)


感谢您的回答。层示例是我正在寻找的最接近的答案,确实是重写getter的好选择。然而,从技术上讲,在GETTER方法中并没有真正“设置”属性(这就是问题所在的概念)...是否有时候这是有益的,还是这是一种不良实践? - PostCodeism
更新的代码是否与您所看到的完全相同?在延迟初始化时(请参见我的-imageThatTakesAwhileToGenerate示例),在getter中设置ivar是很常见的,因此并不一定是“不好的”。但是,您粘贴的示例存在问题。让我在我的帖子中添加另一个示例,以尝试展示NSUserDefaults的用法。 - iccir
我仍然认为你没有理解我的问题,尽量不要过于复杂化。我的问题是 - 在GETTER中设置任何东西(忽略它使用了NSUserDefaults,纯属假设)是否是最佳实践?因为对我来说,除了返回之外,你上面的-(CGFloat)width方法中的所有内容都可以放在setWidth方法中... - PostCodeism
1
在非常简单的层面上:不,将值设置在getter中不是标准做法。本页中我和其他人提供的所有示例都是根据情况使用的工具。最终,最佳实践就是能够发布没有错误的版本;如果你正在与初级的Obj-C工程师打交道,我建议保持getter / setter简单,在显式方法(-loadValuesFromNSUserDefaultsNow)中进行加载。但是,请记住,这个页面上的所有模式都可以被系统框架所使用。 - iccir
这是一个非常圆滑的不表态的回答 ;) 虽然感谢您的意见。也许SO并不是询问灰色设计相关问题的最佳场所,最好留给询问黑白实现问题的人。干杯! - PostCodeism

5
第一个问题是您不想使用getWidth。在objC中的模式是name和setName。不要使用getName。它会干扰绑定和KVO。
另外,如果只是设置/获取iVar,没有理由进行覆盖。如果您正在执行额外的处理/验证,则可能可以覆盖。
编辑:
您还应尽量避免在getter中设置数据和进行重型处理。getter应该封装一些状态并返回数据。预期它非常轻量级。应该在方法或setter中完成重型处理和/或状态修改。例如,人们在getter上设置调试监视器,并不希望进行重型处理和状态修改。

谢谢。记录一下,我从来不在访问器名称后添加get,那只是深夜的语义学。让我们不要被问题所分心……那就是,在GETTER方法中有必要SET属性吗? - PostCodeism
4
懒加载是一种设计模式的实例,其中您可能需要自己实现getter方法。 - Ricky Helgesson

1

如果您惰性地创建属性对象,该怎么办呢? 我经常使用这种模式,也在Xcode的CoreData模板中使用过。

- (NSString *)string
{
    if (!_string) {
        // Create the string property lazily
        // Create is using some other internal, etc values
        _string = [NSString alloc] initWith...]
    }
    return _string;
}

另外:

- (void)setString:(NSString *)string
{
    if (![string isEqualToString:_string]) {
        // Probably you want to make your property observable here too :)

        [_setString release];
        _setString = [string retain];

        // Update other things that depend on _string for example redraw the view, etc
        [self setNeedsDisplay];
    }
}

0

有很多理由重写getter和setter方法,例如当我制作自定义UITableViewCell对象时,我会重写setter方法,这样我就可以设置一个属性,然后在该setter方法中,它将自动更新单元格内的标签或其他内容,而不是在设置属性后调用更新函数。

如果您想以不同于接收信息的方式存储信息,例如电话号码对象,您可能希望覆盖getter。您可以将其存储为5551234567,然后自动检索为555-123-4567或类似的格式。我很少重写getter方法,但我经常重写setter方法。


这在你有使用自定义数据对象的自定义UI元素时特别有用,这就是为什么我不仅仅将UI组件属性作为它们自己,而是更容易地使用数据对象并覆盖setter。 - Patrick T Nelson
在这里不是关于重写setter的问题。重写getter以设置属性才是问题... - PostCodeism

0

这是面向对象编程中完全可接受的做法。但是,需要注意副作用。例如,在setter方法中不应该执行像网络访问这样的操作。

然而,在您上面列出的代码中,由于它们与合成方法所做的事情没有任何区别,因此没有理由包含实现。它们只会使代码混乱。


你的第一段话很有可信度。你可能已经发现了一个真正的例子,应该在GETTER方法中SET属性。请不要被上面的代码分散太多注意力,那只是我脑海中的一个深夜例子。无论如何,我已经更新了代码示例,以更好地解释所讨论的概念。 - PostCodeism

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接