你为什么要用静脉注射?

我通常会看到这个问题以另一种方式提出,比如 每个变量都必须是一个属性吗?(我喜欢 bbum 对这个问题的回答)。

我几乎只在代码中使用属性。然而,我偶尔会和一个承包商合作,他已经在 iOS 上开发了很长时间,是一个传统的游戏程序员。他编写的代码几乎没有声明任何属性,而是依赖于 ivar。我猜他这么做是因为1。)他已经习惯了,因为在 Objective C 2.0(2007年10月)和2之前,属性并不总是存在。)为了不通过 getter/setter 获得最小的性能增益。

虽然他编写的代码不会泄漏,但我仍然希望他使用属性而不是 ivar。我们讨论过这个问题,他或多或少认为没有理由使用属性,因为我们没有使用 KVO,而且他有处理内存问题的经验。

我的问题是... 为什么你会想要使用 IVAR 时期-经验丰富与否。使用 ivar 真的有那么大的性能差异吗?

另外,作为澄清的一点,我根据需要覆盖 setter 和 getter,并使用与 getter/setter 内部的属性相关联的 ivar。但是,在 getter/setter 或 init 之外,我总是使用 self.myProperty语法。


编辑1

我很感激所有的好回答。我想解决的一个看起来不正确的问题是,使用一个 ivar 可以得到封装,而使用一个属性则不能。只需在类继续中定义属性。这样就不会被外人发现。您还可以在接口中声明该属性 readonly,并在实现中将其重新定义为 readwrite,如下所示:

// readonly for outsiders
@property (nonatomic, copy, readonly) NSString * name;

并在课堂上继续:

// readwrite within this file
@property (nonatomic, copy) NSString * name;

要使它完全“私有”,只能在类继续中声明它。

36619 次浏览

封装

如果这个 ivar 是私有的,那么程序的其他部分就不能很容易地得到它。使用声明的属性,聪明的人可以很容易地通过访问器进行访问和变异。

表演

是的,在某些情况下,这可能会有所不同。有些程序有一些约束,在这些约束中,它们不能在程序的某些部分使用任何 obc 消息传递(考虑实时性)。在其他情况下,您可能希望直接访问它以提高速度。在其他情况下,这是因为 obc 消息传递充当了优化防火墙。最后,它可以减少引用计数操作并最小化峰值内存使用(如果正确的话)。

非平凡类型

例如: 如果您使用 C + + 类型,直接访问有时是更好的方法。该类型可能不是可复制的,或者复制可能并非易事。

多线程

你的很多变种都是相互依赖的。必须确保在多线程上下文中的数据完整性。因此,您可能倾向于直接访问关键部分中的多个成员。如果您坚持使用访问器来访问相互依赖的数据,那么您的锁通常必须是可重入的,并且您最终通常会进行更多的收购(有时候会显著地增加)。

程序正确性

由于子类可以覆盖任何方法,因此您最终可能会发现,在写入接口与适当地管理状态之间存在语义上的差异。直接访问程序的正确性在部分构造的状态中尤其常见——在初始化器和 dealloc中,最好使用直接访问。在访问器、方便构造函数、 copymutableCopy和存档/序列化实现的实现中,您可能也会发现这种情况。

随着人们从 一切都有一个公共读写访问器思维模式转向能够很好地隐藏实现细节/数据的模式,这种情况也会更加频繁。有时候,为了做正确的事情,你需要正确地避开子类的覆盖可能引入的副作用。

二进制大小

当考虑程序的执行时,默认情况下声明所有的 readwrite 通常会导致许多您永远不需要的访问器方法。因此,它将添加一些脂肪到您的程序和加载时间以及。

最小化复杂性

在某些情况下,完全没有必要为一个简单的变量添加 + type + 维护所有额外的脚手架,例如在一个方法中编写并在另一个方法中读取的私有 bool。


这并不是说使用属性或访问器是不好的——每个都有重要的好处和限制。与许多面向对象语言和设计方法一样,您也应该支持在 OBC 中具有适当可见性的访问器。有时候你需要改变一下。因此,我认为最好限制对声明 ivar 的实现的直接访问(例如,声明它为 @private)。


重新编辑1:

我们大多数人已经记住了如何动态调用隐藏访问器(只要我们知道名称...)。与此同时,我们大多数人都记住了 没有如何正确访问不可见的变量(KVC 以外)。类继续 有帮助,但它确实引入了漏洞。

这种变通办法显而易见:

if ([obj respondsToSelector:(@selector(setName:)])
[(id)obj setName:@"Al Paca"];

现在只使用一个 ivar,不使用 KVC。

最重要的原因是 信息隐藏的 OOP 概念: 如果您通过属性公开所有内容,从而允许外部对象窥视另一个对象的内部,那么您将利用这些内部对象,从而使更改实现变得复杂。

“最小性能”的收益可以迅速总结,然后成为一个问题。我的经验告诉我,我开发的一个应用程序确实将 iDevice 发挥到了极致,因此我们需要避免不必要的方法调用(当然只有在合理可能的情况下)。为了帮助实现这个目标,我们还避免使用点语法,因为它使得第一眼很难看到方法调用的数量: 例如,表达式 self.image.size.width触发了多少方法调用?相比之下,你可以立即告诉与 [[self image] size].width

另外,使用正确的 ivar 命名,KVO 可以不使用属性(IIRC,我不是 KVO 专家)。

属性将变量公开给其他类。如果你只需要一个与你创建的类相关的变量,使用实例变量。这里有一个小例子: 用于解析 RSS 的 XML 类以及通过一系列委托方法等循环的类似方法。有一个 NSMutableString 实例来存储解析的每次不同传递的结果是切实可行的。外部类没有理由需要访问或操作该字符串。因此,您只需在头部声明它或者私下声明它,然后在整个类中访问它。为其设置一个属性可能只有在确保没有内存问题时才有用,使用 self. mutableString 调用 getter/setter。

向后兼容性对我来说是一个因素。我不能使用 Objective-C 2.0的任何特性,因为我开发的软件和打印机驱动程序必须在 Mac OS X 10.3上工作,作为需求的一部分。我知道你的问题似乎针对 iOS 系统,但我想我仍然会分享我不使用属性的原因。

属性与实例变量之间是一种权衡,最终取决于应用程序。

封装/信息隐藏 从设计的角度来看,这是一件好事(TM) ,狭窄的接口和最小的链接使得软件可维护和可理解。在 Obj-C 中很难隐藏任何东西,但是在 实施中声明的实例变量非常接近。

性能 虽然“过早的优化”是一件坏事(TM) ,但仅仅因为你能写出来,就写出性能糟糕的代码也同样糟糕。很难反驳方法调用比加载或存储更昂贵的说法,而且在计算密集型代码中,成本很快就会增加。

在具有属性的静态语言(如 C #)中,编译器通常可以优化对 setter/getter 的调用。然而,Obj-C 是动态的,删除这样的调用要困难得多。

在 Obj-C 中反对实例变量的一个参数传统上是内存管理。对于 MRC 实例变量,需要调用保留/发布/自动发布来遍布整个代码,属性(合成与否)将 MRC 代码保留在一个地方——抽象原则是一个好东西(TM)。但是对于 GC 或 ARC,这个参数消失了,因此内存管理的抽象不再是 反对实例变量的参数。

对我来说通常是表演。访问对象的 ivar 与使用指向包含这种 struct 的内存的指针访问 C 中的 struct 成员一样快。事实上,Objective-C 对象基本上是位于动态分配的内存中的 C 结构。这通常是您的代码所能达到的最快速度,即使是手工优化的程序集代码也不可能比这更快。

通过 getter/set 访问 ivar 需要使用 Objective-C 方法调用,这比“正常”的 C 函数调用要慢得多(至少3-4倍) ,即使是正常的 C 函数调用也比访问 struct 成员慢好几倍。根据属性的不同,编译器生成的 setter/getter 实现可能涉及对函数 objc_getProperty/objc_setProperty的另一个 C 函数调用,因为这些函数必须根据需要调用对象 retain/copy/autorelease,并在必要时进一步执行原子属性的自旋锁。这很容易变得非常昂贵,我不是说50% 慢。

让我们试试这个:

CFAbsoluteTime cft;
unsigned const kRuns = 1000 * 1000 * 1000;


cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
testIVar = i;
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"1: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);


cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
[self setTestIVar:i];
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"2: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

产出:

1: 23.0 picoseconds/run
2: 98.4 picoseconds/run

这是4.28倍的速度,这是一个非原子原语 int,基本上是 最好的情况; 大多数其他情况甚至更糟(尝试使用原子 NSString *属性!).因此,如果你能够接受这样一个事实,即每个 ivar 访问的速度比实际要慢4-5倍,那么使用属性是可以的(至少在性能方面) ,然而,在很多情况下,这样的性能下降是完全不可接受的。

更新2015-10-20

有些人认为,这不是一个真实世界的问题,上面的代码纯粹是合成的,在真实的应用程序中你永远不会注意到这一点。好吧,那我们来试试真实世界的样本。

下面的代码定义了 Account对象。帐户具有描述其所有者的名称(NSString *)、性别(enum)和年龄(unsigned)以及余额(int64_t)的属性。帐户对象具有 init方法和 compare:方法。compare:方法的定义是: 雌性先于雄性,名称按字母顺序排列,幼年先于老年,平衡顺序从低到高。

实际上有两个帐户类,AccountAAccountB。如果查看它们的实现,您会注意到它们几乎完全相同,只有一个例外: compare:方法。AccountA对象通过方法(getter)访问 他们自己的财产,而 AccountB对象通过 ivar 访问 他们自己的财产。这是唯一的区别!它们都通过 getter 访问其他对象的属性进行比较(通过 ivar 访问是不安全的!如果另一个对象是一个子类并且已经覆盖了 getter,该怎么办?).还要注意,作为 ivars 不会破坏封装访问您自己的属性(ivar 仍然不是公共的)。

测试设置非常简单: 创建1个 Mio 随机帐户,将它们添加到一个数组中并对该数组进行排序。就是这样。当然,有两个数组,一个用于 AccountA对象,一个用于 AccountB对象,两个数组都填充了相同的帐户(相同的数据源)。我们计算对数组进行排序所需的时间。

下面是我昨天几次运行的结果:

runTime 1: 4.827070, 5.002070, 5.014527, 5.019014, 5.123039
runTime 2: 3.835088, 3.804666, 3.792654, 3.796857, 3.871076

正如您可以看到的,对 AccountB对象的数组排序比对 AccountA对象的数组排序更像是 总是显著的更快

不管是谁声称运行时差异高达1.32秒没有任何区别,最好永远不要做 UI 编程。例如,如果我想改变一个大表的排序顺序,像这样的时间差确实会对用户产生巨大的影响(可接受的 UI 和缓慢的 UI 之间的差异)。

此外,在这种情况下,示例代码是在这里执行的唯一真正的工作,但是您的代码有多少次只是一个复杂的发条装置的小齿轮呢?如果每个齿轮都像这样减慢整个过程,那么最终整个发条的速度意味着什么呢?特别是如果一个工作步骤依赖于另一个工作步骤的产出,这意味着所有的低效率将被总结。大多数效率低下本身并不是一个问题,而是它们的总和成为了整个过程的一个问题。这样的问题是剖析器不会轻易显示出来的,因为剖析器的工作就是寻找关键的热点,但是这些效率低下的问题本身都不是热点。CPU 时间只是平均分布在它们之间,但是每个 CPU 只有这么一小部分,优化它似乎完全是浪费时间。这是事实,仅仅优化其中的一个绝对没有任何帮助,而优化所有这些方法可以极大地帮助。

即使您不考虑 CPU 时间,因为您认为浪费 CPU 时间是完全可以接受的,毕竟“它是免费的”,那么由于功耗而导致的服务器托管成本又如何呢?那么移动设备的电池运行时间呢?如果你写同一个移动应用两次(比如一个自己的移动浏览器) ,一次是所有类只能通过 getter 访问他们自己的属性的版本,一次是所有类只能通过 ivar 访问它们的版本,持续使用第一个肯定会比使用第二个消耗电池快得多,即使它们是功能等同的,对用户来说第二个甚至可能会感觉更快一些。

下面是 main.m文件的代码(该代码依赖于启用 ARC,并且在编译时一定要使用优化以查看完整效果) :

#import <Foundation/Foundation.h>


typedef NS_ENUM(int, Gender) {
GenderMale,
GenderFemale
};




@interface AccountA : NSObject
@property (nonatomic) unsigned age;
@property (nonatomic) Gender gender;
@property (nonatomic) int64_t balance;
@property (nonatomic,nonnull,copy) NSString * name;


- (NSComparisonResult)compare:(nonnull AccountA *const)account;


- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance;
@end




@interface AccountB : NSObject
@property (nonatomic) unsigned age;
@property (nonatomic) Gender gender;
@property (nonatomic) int64_t balance;
@property (nonatomic,nonnull,copy) NSString * name;


- (NSComparisonResult)compare:(nonnull AccountB *const)account;


- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance;
@end




static
NSMutableArray * allAcocuntsA;


static
NSMutableArray * allAccountsB;


static
int64_t getRandom ( const uint64_t min, const uint64_t max ) {
assert(min <= max);
uint64_t rnd = arc4random(); // arc4random() returns a 32 bit value only
rnd = (rnd << 32) | arc4random();
rnd = rnd % ((max + 1) - min); // Trim it to range
return (rnd + min); // Lift it up to min value
}


static
void createAccounts ( const NSUInteger ammount ) {
NSArray *const maleNames = @[
@"Noah", @"Liam", @"Mason", @"Jacob", @"William",
@"Ethan", @"Michael", @"Alexander", @"James", @"Daniel"
];
NSArray *const femaleNames = @[
@"Emma", @"Olivia", @"Sophia", @"Isabella", @"Ava",
@"Mia", @"Emily", @"Abigail", @"Madison", @"Charlotte"
];
const NSUInteger nameCount = maleNames.count;
assert(maleNames.count == femaleNames.count); // Better be safe than sorry


allAcocuntsA = [NSMutableArray arrayWithCapacity:ammount];
allAccountsB = [NSMutableArray arrayWithCapacity:ammount];


for (uint64_t i = 0; i < ammount; i++) {
const Gender g = (getRandom(0, 1) == 0 ? GenderMale : GenderFemale);
const unsigned age = (unsigned)getRandom(18, 120);
const int64_t balance = (int64_t)getRandom(0, 200000000) - 100000000;


NSArray *const nameArray = (g == GenderMale ? maleNames : femaleNames);
const NSUInteger nameIndex = (NSUInteger)getRandom(0, nameCount - 1);
NSString *const name = nameArray[nameIndex];


AccountA *const accountA = [[AccountA alloc]
initWithName:name age:age gender:g balance:balance
];
AccountB *const accountB = [[AccountB alloc]
initWithName:name age:age gender:g balance:balance
];


[allAcocuntsA addObject:accountA];
[allAccountsB addObject:accountB];
}
}




int main(int argc, const char * argv[]) {
@autoreleasepool {
@autoreleasepool {
NSUInteger ammount = 1000000; // 1 Million;
if (argc > 1) {
unsigned long long temp = 0;
if (1 == sscanf(argv[1], "%llu", &temp)) {
// NSUIntegerMax may just be UINT32_MAX!
ammount = (NSUInteger)MIN(temp, NSUIntegerMax);
}
}
createAccounts(ammount);
}


// Sort A and take time
const CFAbsoluteTime startTime1 = CFAbsoluteTimeGetCurrent();
@autoreleasepool {
[allAcocuntsA sortedArrayUsingSelector:@selector(compare:)];
}
const CFAbsoluteTime runTime1 = CFAbsoluteTimeGetCurrent() - startTime1;


// Sort B and take time
const CFAbsoluteTime startTime2 = CFAbsoluteTimeGetCurrent();
@autoreleasepool {
[allAccountsB sortedArrayUsingSelector:@selector(compare:)];
}
const CFAbsoluteTime runTime2 = CFAbsoluteTimeGetCurrent() - startTime2;


NSLog(@"runTime 1: %f", runTime1);
NSLog(@"runTime 2: %f", runTime2);
}
return 0;
}






@implementation AccountA
- (NSComparisonResult)compare:(nonnull AccountA *const)account {
// Sort by gender first! Females prior to males.
if (self.gender != account.gender) {
if (self.gender == GenderFemale) return NSOrderedAscending;
return NSOrderedDescending;
}


// Otherwise sort by name
if (![self.name isEqualToString:account.name]) {
return [self.name compare:account.name];
}


// Otherwise sort by age, young to old
if (self.age != account.age) {
if (self.age < account.age) return NSOrderedAscending;
return NSOrderedDescending;
}


// Last ressort, sort by balance, low to high
if (self.balance != account.balance) {
if (self.balance < account.balance) return NSOrderedAscending;
return NSOrderedDescending;
}


// If we get here, the are really equal!
return NSOrderedSame;
}


- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance
{
self = [super init];
assert(self); // We promissed to never return nil!


_age = age;
_gender = gender;
_balance = balance;
_name = [name copy];


return self;
}
@end




@implementation AccountB
- (NSComparisonResult)compare:(nonnull AccountA *const)account {
// Sort by gender first! Females prior to males.
if (_gender != account.gender) {
if (_gender == GenderFemale) return NSOrderedAscending;
return NSOrderedDescending;
}


// Otherwise sort by name
if (![_name isEqualToString:account.name]) {
return [_name compare:account.name];
}


// Otherwise sort by age, young to old
if (_age != account.age) {
if (_age < account.age) return NSOrderedAscending;
return NSOrderedDescending;
}


// Last ressort, sort by balance, low to high
if (_balance != account.balance) {
if (_balance < account.balance) return NSOrderedAscending;
return NSOrderedDescending;
}


// If we get here, the are really equal!
return NSOrderedSame;
}


- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance
{
self = [super init];
assert(self); // We promissed to never return nil!


_age = age;
_gender = gender;
_balance = balance;
_name = [name copy];


return self;
}
@end

语义学

  • @property能够表达的是 IVAR 不能表达的: nonatomiccopy
  • 什么样的变量能够表达 @property不能表达的:
    • @protected: 公共的子类,私人的外部。
    • @package: 在64位框架上为公共,在外部为私有。在32位上与 @public相同。参见苹果的 64位类和实例变量访问控制
    • 例如,强对象引用的数组: id __strong *_objs

表演

短篇小说: ivar 速度更快,但是对于大多数用途来说并不重要。nonatomic属性不使用锁,但是直接 ivar 更快,因为它跳过了访问器调用。有关详细信息,请阅读以下来自 lists.apple.com 的 电子邮件

Subject: Re: when do you use properties vs. ivars?
From: John McCall <email@hidden>
Date: Sun, 17 Mar 2013 15:10:46 -0700

属性在很多方面影响性能:

  1. 正如已经讨论过的,发送消息来加载/存储是 比仅仅内联加载/存储更慢

  2. 发送消息来执行装载/存储也是需要保存在 i-cache 中的 更多的代码: 即使 getter/setter 除了加载/存储之外,没有添加任何额外的指令 在调用者中设置 消息发送并处理结果

  3. 发送消息会强制将该选择器的条目保存在 方法缓存中,而该内存通常会保存在 方法缓存中 这增加了启动时间,增加了静态内存 使用你的应用程序,并使上下文切换更加痛苦。因为 方法缓存特定于对象的动态类,则此 使用 KVO 的次数越多,问题就越严重。

  4. 发送消息 强制将函数中的所有值溢出到堆栈中(或保存在被调用方保存寄存器中,这意味着 在不同的时间溢出)

  5. 发送 消息可能有任意的副作用因此

    • 强制编译器重置关于非本地内存的所有假设
    • 不能被吊起、沉没、重新排序、合并或消除。 < br/> < br/> < br/>
  6. 在 ARC 中,通过被调用方或调用方返回 消息发送的结果将始终保留,即使 + 0返回: 即使 方法不保留/自动发布它的结果,调用者不知道 我们必须采取行动来阻止这种情况的发生 这是不可能被消除的,因为消息发送是 不能静态分析

  7. 在 ARC 中,因为 setter 方法的参数通常是 + 0,所以没有办法“传递”对该对象的保留(如 上面讨论过,ARC 通常有)转换成 IVAR,因此 < strong > 值 一般要保留/释放两次。

当然,这并不意味着它们总是坏的ーー有一种 有很多使用属性的好理由。只要记住,比如 许多其他的语言功能,它们并不是免费的。


约翰。