为什么要使用ivar?


153

我通常会以相反的方式问这个问题,例如,每个ivar都必须是一个属性吗?(而且我喜欢bbum对这个问题的回答)。

我几乎只在代码中使用属性。但是,我经常与一个承包商合作,该承包商已经在iOS上开发了很长时间,并且是传统的游戏程序员。他编写的代码几乎不声明任何属性,而是依靠ivars。我认为他这样做是因为1.)他习惯了,因为直到Objective C 2.0('Oct '07)和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;

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


2
支持一个有趣的问题-好吧,还有一个我想听听ivars的问题,因为听起来我已经被教导要按照Sam的方式来做。
2012年

2
请注意,自动引用计数(ARC)将对ivars的内存管理优势与属性一样,因此,在ARC代码中,差异实际上与封装有关。
benzado 2012年

1
您的问题,尤其是“编辑1”部分实际上比选择的答案提供更多信息。
user523234'5

1
To Edit1:我认为即使使用.h进行键值编码,也可以读取和写入每个属性,即使在.h中只有一个只读声明,例如:[object setValue:[NSNumber numberWithInt:20] forKey:@“ propertyname “];
Binarian

1
@Sam编辑1:如果使用私有属性并在.m文件中使用类扩展/继续,则对于子类不可见。您需要再次编写代码或使用带有扩展名的其他.h。使用@ protected / default更容易。
Binarian

Answers:


100

封装形式

如果ivar是私有的,则程序的其他部分将很难实现。拥有已声明的属性,聪明的人可以通过访问器轻松访问和变异。

性能

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

非平凡类型

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

多线程

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

程序正确性

由于子类可以覆盖任何方法,因此您最终可能会看到在写接口与适当管理状态之间存在语义上的区别。直接访问程序正确性在部分构造的状态中尤其常见-在初始化程序和中dealloc,最好使用直接访问。您也可以在访问,便利的构造函数,的实现找到这个共同的copymutableCopy和归档/序列化的实现。

所有具有公共读写访问器思想的事物转移到很好地隐藏其实现细节/数据的事物,它也变得更加频繁。有时您需要正确地避免副作用,子类的重写可能会引入该错误,以执行正确的操作。

二进制大小

考虑到程序的执行时间,默认情况下将所有内容都声明为可读写通常会导致许多不需要的访问器方法。因此,这也会给程序和加载时间增加一些负担。

减少复杂性

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


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


重新编辑1:

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

这种解决方法很明显:

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

现在,仅在不使用KVC的情况下尝试使用ivar。


@山姆,谢谢,很好的问题!再复杂性:它肯定是双向的。重新封装-更新
贾斯汀

@bbum RE:冒昧的例子尽管我同意你的观点,这是错误的解决方案,但我无法想象有很多有经验的objc开发人员相信这不会发生。我已经在其他人的程序中看到了这一点,并且App Store甚至禁止使用私有Apple API。
贾斯汀

1
您无法使用object-> foo访问私有ivar吗?不难记住。
尼克·洛克伍德

1
我的意思是,您可以使用C->语法,使用对象引用的指针进行访问。Objective-C类基本上只是底层的结构,并且给出了指向该结构的指针,用于访问成员的C语法为->,它也适用于目标C类中的ivars。
Nick Lockwood

1
@NickLockwood如果ivar为@private,则编译器应禁止在类和实例方法之外进行成员访问-那不是您看到的内容吗?
贾斯汀2012年

76

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

通过吸气器/设置访问ivar涉及到Objective-C方法调用,这比“常规” C函数调用要慢得多(至少3-4倍),甚至普通C函数调用也已经比其慢很多倍。访问结构成员。根据你的财产的属性,由编译器生成的setter /吸气实施可能需要另一个C函数调用函数objc_getProperty/ objc_setProperty,因为这些将不得不retain/ copy/ autorelease对象根据需要,进一步进行spinlocking用于原子性质必要。这很容易变得非常昂贵,我并不是说要慢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对象访问自己的属性通过方法(吸气剂),而AccountB对象访问自己的属性由伊娃。那真的是唯一的区别!它们都访问另一个对象的属性,以通过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

3
极为翔实和扎实的解释。支持代码示例
Philip007

1
我在您的帖子中看到的关键限定词之一是“ ...来自关键代码路径”。关键是要使用使代码更易于阅读/编写的代码,然后优化发现的关键路径。这将在需要的地方增加复杂性。
桑迪·查普曼

1
@ViktorLexington在我的代码中unsigned int,无论您是否使用ARC,我都设置了一个永远不会保留/发布的值。保留/释放本身很昂贵,因此,由于保留管理会直接使用setter / getter或ivar来添加始终存在的静态开销,因此差异将较小。但是,如果直接访问ivar,您仍然可以节省一个额外的方法调用的开销。在大多数情况下,这没什么大不了的,除非您每秒要做几千次。苹果公司表示,除非您处于init / dealloc方法或发现瓶颈,否则默认情况下使用getters / setters。
Mecki 2014年

1
@Fogmeister添加了一个代码示例,该示例显示了如何在一个非常简单的真实示例中轻松实现巨大变化。这个示例与进行数万亿次计算的超级计算机无关,更多的是关于对一个非常简单的数据表进行排序(在数百万个应用程序中很常见)。
梅基2015年

2
标记为@malhal的属性copy不是每次你访问它时使其值的副本。的吸气copy特性是像的吸气strong/ retain属性。它的代码基本上是return [[self->value retain] autorelease];。只有二传手副本的价值和它大致会是这样的[self->value autorelease]; self->value = [newValue copy];,而strong/ retainsetter方法如下所示:[self->value autorelease]; self->value = [newValue retain];
Mecki

9

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

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

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


3
+1关于“最低性能”的良好响应得到了加分,并希望明确看到所有方法调用。将dot语法与属性一起使用肯定会掩盖自定义getter / setter方法中的许多工作(尤其是如果该getter方法每次都返回某项内容的副本时)。
2012年

1
不使用设置器,KVO对我不起作用。直接更改ivar并不会告诉观察者该值已更改!
Binarian

2
KVC可以访问ivars。KVO无法检测到ivars的更改(而是依赖于调用访问器)。
Nikolai Ruhe

9

语义学

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

性能

简短的故事:ivars更快,但是对于大多数用途而言并不重要。nonatomic属性不使用锁,但是直接ivar更快,因为它跳过了accessors调用。有关详细信息,请阅读来自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. 发送消息会强制将该选择器的条目保留在方法缓存中,并且该内存通常会停留在d-cache中。这会增加启动时间,增加应用程序的静态内存使用率,并使上下文切换更加痛苦。由于方法高速缓存是特定于对象的动态类的,因此您在该对象上使用KVO的次数越多,此问题就会增加。

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

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

    • 强制编译器重置所有关于非本地内存的假设
    • 无法吊起,沉没,重新排序,合并或消除。

  6. 在ARC中,即使+0返回,消息的发送结果也始终由被调用方或调用方保留,即使该方法不保留/自动释放其结果,调用方也不知道并具有尝试采取措施以防止结果自动发布。由于无法静态分析邮件发送,因此永远无法消除这种情况。

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

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


约翰。


6

属性与实例变量是一个折衷,最终选择取决于应用程序。

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

性能尽管“过早的优化”是一件不好的事情(TM),但仅仅因为您可以做到而编写性能不佳的代码至少也同样糟糕。很难辩称方法调用比加载或存储要昂贵,而且在计算密集型代码中,成本很快就加起来了。

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

抽象传统上,Obj-C中针对实例变量的争论是内存管理。使用MRC实例变量时,要求将保留/释放/自动释放的调用分散在整个代码中,属性(无论是否合成)将MRC代码都保留在一个位置-抽象的原理是一件好事(TM)。但是,对于GC或ARC,此参数消失了,因此内存管理的抽象不再是针对实例变量的参数。


5

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


5

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.