154

我通常看到这个问题以另一种方式提出,例如Must every ivar be a property? (我喜欢 bbum 对这个 Q 的回答)。

我几乎只在我的代码中使用属性。然而,每隔一段时间,我就会与一个长期在 iOS 上进行开发并且是传统游戏程序员的承包商一起工作。他编写的代码几乎不声明任何属性,并且依赖于 ivars。我认为他这样做是因为 1.) 他已经习惯了它,因为直到 Objective C 2.0(07 年 10 月)和 2.) 属性并不总是存在,因为不通过 getter/setter 的性能增益最小。

虽然他编写的代码不会泄漏,但我仍然希望他使用属性而不是 ivars。我们谈到了它,他或多或少认为没有理由使用属性,因为我们没有使用 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;

要让它完全“私有”,只需在类延续中声明它。

4

7 回答 7

100

封装

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

表现

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

非平凡类型

示例:如果您有 C++ 类型,则直接访问有时只是更好的方法。该类型可能不可复制,或者复制起来可能并不简单。

多线程

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

程序正确性

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

它也更频繁,因为人们从一切都具有公共读写访问器的心态转变为很好地隐藏其实现细节/数据的心态。有时您需要正确地绕过子类覆盖可能引入的副作用,以便做正确的事情。

二进制大小

当您暂时考虑程序的执行时,默认情况下声明所有内容为 readwrite 通常会导致许多您永远不需要的访问器方法。所以它会给你的程序和加载时间增加一些脂肪。

最小化复杂性

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


这并不是说使用属性或访问器不好——每个都有重要的好处和限制。像许多 OO 语言和设计方法一样,您还应该支持在 ObjC 中具有适当可见性的访问器。有时你需要偏离。出于这个原因,我认为通常最好限制对声明 ivar 的实现的直接访问(例如 declare it @private)。


重新编辑1:

我们大多数人都记住了如何动态调用隐藏的访问器(只要我们知道名称……)。同时,我们大多数人还没有记住如何正确访问不可见的 ivars(除了 KVC)。类延续有帮助,但它确实引入了漏洞。

这种解决方法很明显:

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

现在只用 ivar 试试,不带 KVC。

于 2012-01-31T20:55:43.233 回答
76

对我来说,这通常是性能。访问对象的 ivar 与使用指向包含此类结构的内存的指针访问 C 中的结构成员一样快。事实上,Objective-C 对象基本上是位于动态分配内存中的 C 结构。这通常与您的代码一样快,甚至手动优化的汇编代码也不会比这更快。

通过 getter/setting 访问 ivar 涉及到一个 Objective-C 方法调用,它比“普通”C 函数调用慢得多(至少 3-4 倍),即使是普通 C 函数调用也已经慢了好几倍访问结构成员。根据您的属性的属性,编译器生成的 setter/getter 实现可能涉及对函数objc_getProperty/的另一个 C 函数调用objc_setProperty,因为这些函数必须根据需要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 访问不会破坏封装(ivars 仍然不公开)。

测试设置非常简单:创建 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时间是完全可以接受的,毕竟“它是免费的”,那么由功耗引起的服务器托管成本呢?移动设备的电池运行时间如何?如果您要编写相同的移动应用程序两次(例如,自己的移动网络浏览器),一次是所有类仅通过 getter 访问自己的属性的版本,一次是所有类仅通过 ivars 访问它们的版本,那么不断使用第一个肯定会耗尽电池比使用第二个电池快得多,即使它们在功能上是等效的,并且对于用户来说,第二个电池甚至可能会感觉更快一些。

现在这是您的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
于 2012-07-10T20:48:55.290 回答
9

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

“最小的性能”增益可以很快总结,然后成为一个问题。我从经验中知道;我正在开发一个真正将 iDevices 发挥到极致的应用程序,因此我们需要避免不必要的方法调用(当然只有在合理可能的情况下)。为了帮助实现这一目标,我们还避免使用点语法,因为它让人很难一眼看出方法调用的数量:例如,表达式self.image.size.width触发了多少方法调用?相比之下,您可以立即用 来判断[[self image] size].width

此外,通过正确的 ivar 命名,可以在没有属性的情况下使用 KVO(IIRC,我不是 KVO 专家)。

于 2012-01-31T21:00:08.173 回答
9

语义

  • 什么@property可以表达 ivars 不能:nonatomiccopy.
  • ivars 可以表达的@property不能:
    • @protected: 子类公开,外部私有。
    • @package:在 64 位框架上公开,外部私有。与@public32 位相同。请参阅 Apple 的64 位类和实例变量访问控制
    • 预选赛。例如,强对象引用数组:id __strong *_objs.

表现

短篇小说: ivars 更快,但对于大多数用途来说并不重要。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. 正如已经讨论过的,发送消息来执行加载/存储比仅执行加载/存储 inline 慢

  2. 发送消息进行加载/存储也需要在 i-cache 中保留更多代码:即使 getter/setter 除了加载/存储之外添加了零个额外指令,也会有一半- 调用者中的十几个额外指令来设置消息发送和处理结果。

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

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

  5. 发送消息可能会产生任意副作用,因此

    • 强制编译器重置所有关于非本地内存的假设
    • 不能被提升、沉没、重新排序、合并或消除。

  6. 在 ARC 中,消息发送的结果将始终由被调用者或调用者保留,即使是 +0 返回:即使该方法不保留/自动释放其结果,调用者也不知道并且有尝试采取措施防止结果自动释放。这永远无法消除,因为消息发送不是静态可分析的。

  7. 在 ARC 中,由于 setter 方法通常将其参数设为 +0,因此无法将该对象的保留(如上所述,ARC 通常具有)“转移”到 ivar,因此该值通常必须得到保留/释放两次

当然,这并不意味着它们总是不好的——使用属性有很多好的理由。请记住,与许多其他语言功能一样,它们不是免费的。


约翰。

于 2013-05-21T07:38:41.680 回答
6

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

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

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

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

抽象Obj-C 中反对实例变量的一个论点传统上是内存管理。对于 MRC 实例变量,需要在整个代码中传播保留/释放/自动释放的调用,属性(无论是否合成)将 MRC 代码保留在一个地方 - 抽象原则是一件好事 (TM)。然而,对于 GC 或 ARC,这个论点消失了,因此内存管理的抽象不再是反对实例变量的论点。

于 2012-01-31T21:19:55.637 回答
5

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

于 2012-01-31T21:02:00.917 回答
5

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

于 2012-01-31T21:16:59.697 回答