与Objective-C中的setter方法相比,使用指针式赋值有多危险



假设我有一个简单的类,如下所示:

@interface A { 
// @public
int var;
}
// @property(some_property) int var;
@end

当我想访问变量var时,我有一些选项。如果我公开var,我可以这样做:

A objectA = [ [ A alloc ] init ];
NSLog( @"%d", objectA->var );
objectA->var = someNumber;

如果我把它变成一处房产,我将不得不做一些更像这样的事情:

A objectA = [ [ A alloc ] init ];
NSLog( @"%d", objectA.var ); // dot-syntax
NSLog( @"%d", [ objectA var ] ); // get-syntax
[ objectA setVar: someNumber ];

我尝试过这两种方法,它们都很好,但我的问题是,使用旧式指针表示法访问对象内部的变量有多危险?我以后会担心我现在应该通过标准化方法访问来处理的事情吗?或者,只要它有效,我可以随心所欲地做它吗?

我会投入我自己的2美分,因为我对另一个问题的评论几天前开始了类似的讨论。

您应该始终使用访问器方法在对象类的实现之外设置和获取对象的属性。您应该几乎始终使用访问器方法来访问类的属性,即使在所述类的实现内部也是如此。

以下是一些原因的列表:

  • 封装一个类可能会将一个属性暴露给外部世界,该属性看起来与任何其他属性一样,但内部没有相应的ivar支持。也许它实际上对另一种内部属性进行了某种转换。更重要的是,这种实现可能会在未来的某个时候发生变化。OO代码中封装的主要原因之一是,可以更改类的内部实现,而不需要该类的外部用户进行更改。(就在今天早上,我在一个重要的旧类中做了这样一个更改,从ivar更改为一个完全不同的支持方法。如果一堆其他类,甚至是同一类中的方法直接访问ivar,我将不得不更改比我多得多的代码。)

  • 内存管理这对ARC来说没什么大不了的,因为无论哪种方式都会正确处理它(主要见下文),但对于手动内存管理,属性的访问器方法将负责正确地保留和释放底层对象。将这些代码放在一个地方可以大大降低发生内存管理错误(泄漏和过度释放)的几率,更容易阅读,更容易修改,代码也不那么冗长。即使启用了ARC,使用复制行为声明的属性也依赖于setter方法来执行复制,如果直接进行ivar访问,则可以绕过此方法。

  • 在片场自定义行为这实际上只是封装的另一部分。当调用相应的setter时,类通常想要做一些事情,而不仅仅是将ivar设置为新值。一个非常常见、简单的例子是,当影响其外观的属性发生更改时,NSView子类调用[self setNeedsDisplay]。如果你不调用setter,而是直接设置ivar,你将完全绕过这种行为。同样,你可能会认为这很好,只要你知道setter不需要做这样的事情,但需求会改变,通过从一开始就使用setter,你可以更容易地进行更改。

  • 获取/延迟实例化的自定义行为最常见的原因之一是执行懒惰实例化。你为一个属性实现getter方法,这样它就会检查底层的ivar是否为nil,如果是,它会在返回之前先初始化ivar。下次调用它时,会设置ivar,这样初始化就不会再次发生。这是一种简单的方法,可以将昂贵(CPU密集型和/或内存密集型)对象的创建推迟到实际需要时。例如,它通常是作为一种性能优化来提高启动时间,这是一个完美的封装示例,允许在不破坏外部代码对类的现有使用的情况下对类的实现进行简单改进。

  • KVO合规性Objective-C提供了一种称为键值观察的机制,允许一个对象在任何时候更改另一个对象的给定属性时请求通知。如果您使用正确命名的访问者或合成的@properties,您将自动获得对KVO的支持。但是,要使KVO工作,必须实际调用访问器方法。如果您直接更改ivar,则不会通知该ivar相应属性的观察者。无论你是在设置另一个对象的属性还是在self上设置一个属性,你都不知道观察者是否已经注册了该属性的更改,并且你正在缩短他们收到更改通知的时间。

  • 可读性这并不完全适用于objectA->var示例,它更适用于类自己的实现(var = ...)中的直接ivar访问,其中self->由编译器隐含/插入。问题是,尤其是在长方法中,您可能会看到一个赋值语句,但一眼就不知道被赋值的变量是当前作用域的本地变量,还是实例变量。这可以通过命名约定来缓解,比如在ivars前面加下划线、匈牙利符号等,但Objective-C约定仍然意味着最好使用self.var = ...[self setVar:...](顺便说一句,它们在语义上完全相同)。

  • 为什么不呢有些不使用访问器方法,但我想不出有什么好的理由。键入并没有快到哪里去,因为在变量名前面加上"self"只是不需要那么多打字。由于添加了额外的ObjC消息发送,因此调用访问器会带来性能损失。然而,这个惩罚非常小,当然,过早优化是一件坏事。访问器方法调用的开销足以严重影响应用程序性能的情况非常罕见。

请记住,关于在类的实现中使用直接ivar访问,我使用了一个"几乎"限定符。如果您实现自定义访问器方法(而不是@合成属性),那么您当然必须直接访问访问器实现中的ivar。此外,也有一些人不同意,但直接在类的初始值设定项(-init)和dealloc方法中设置ivar是个好主意(也是苹果的建议)。这样做的原因是,当类没有完全初始化时,您可能希望避免setter的副作用。例如,当KVO观察者可能发现通知对象处于不一致/部分初始化状态时,您不希望他们收到更改通知。

使用旧式指针表示法访问对象内部的变量有多危险?

非常危险。

以后我是否必须担心我现在应该通过标准化方法访问来处理的事情?

是。其他需要担心的事情包括:内存管理(memory.management!!)、"键值观察"不工作等。所有这些都是因为使用->,您可以直接访问实例变量(一些过于严格的OO粉丝甚至会说它违反了封装,事实上,它经常这样做),而点表示法调用适当的getter和setter方法(它们负责体面地管理内存、通知KVO侦听器等)

因此,总而言之,请使用访问器方法(如果您愿意,还可以使用点表示法),不要直接访问实例变量。

你应该使用属性形式。这种方法更标准,所以在团队中会发挥得很好。它还有一个明显的优点,即允许您稍后更改实现,而无需更改调用方使用的接口。

在ARC之前,属性类型引用(使用[thing setMyVar:value];thing.myVar = value;)遵守保留/释放语义,而直接iVar引用(thing ->myVar = value;)则不遵守。然而,它与ARC有点混淆。

是的,使用旧式指针表示法更危险。在Objective-C中,使用点表示法与使用"setVar"方法完全相同。

[objectA setVar:someNumber]; // Exactly the same as objectA.var = someNumber;

当我说"完全相同"时,我是认真的。当你使用objectA.var=someNumber时,你调用的是setter方法(你可以在修改实例变量之前修改和验证值)。当使用指针样式赋值时,您是在直接修改,允许根据应用程序的业务逻辑保存可能错误的值。

换句话说:总是使用Objective-C setter/getter约定(它们存在是出于某种原因)。

我尤其喜欢使用objectA.var=someNumber语法,因为它更容易被其他语言的程序员理解(Objective-C方法调用很奇怪,如果你不知道Objective-C在合成属性时会自动创建setters/getter,那么就很难阅读它们)。

最新更新