Effective OC 2.0读书笔记 第7条:在对象内部尽量直接访问实例变量

Effective OC 2.0是一本非常赞的书,如果让我评分,我绝对给10分。

之前读了一遍Effective OC 2.0这本书,现在的想法是将里面的建议实践到工程中,同时将一些使用心得总结成博客。

本文说的是第7条:在对象内部尽量直接访问实例变量

首先要说明:

类中的成员变量在本文称之为实例变量,用@property+@synthesize可以将该变量声明为属性,实际上就是要求编译器自动为其生成accessor(setter/getter)方法,accessor方法可以被覆写。


比较“通过属性的accessor方法访问”和“直接访问”实例变量

访问实例变量有3种方法:

(1)调用属性的setter/getter方法

(2)使用dot syntax,实际上就是在调用setter/getter方法

(3)直接通过实例变量访问,此时setter/getter方法会被绕过


假设我们有一个Wrestler类:

@interface Wrestler : NSObject

@property (copy, nonatomic) NSString *name; // 将name声明为属性

- (void)smell;

@end

@implementation Wrestler
@synthesize name = _name; // 属性name可以使用实例变量_name直接访问

- (void)setName:(NSString *)aName {
    NSLog(@"Set name");
    _name = [aName copy];
}

- (NSString *)name {
    NSLog(@"Get name");
    return [_name copy];
}

- (void)smell {
    NSLog(@"*** Smelling ***");
    
    // 使用dot syntax访问实例变量
    NSLog(@"%@", self.name);
    
    // 直接调用属性的getter方法
    NSLog(@"%@", [self name]);
    
    // 直接访问实例变量
    NSLog(@"%@", _name);
}

@end

测试代码如下:

        Wrestler *wrestler = [[Wrestler alloc] init];
        
        // 直接调用属性的setter方法
        [wrestler setName:@"John"];
        
        // 使用dot syntax访问name属性
        wrestler.name = @"Cena";
        
        [wrestler smell];

输出如下:

2014-05-04 21:09:44.755 AccessorDemo[1021:303] Set name
2014-05-04 21:09:44.756 AccessorDemo[1021:303] Set name
2014-05-04 21:09:44.757 AccessorDemo[1021:303] *** Smelling ***
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Get name
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Cena
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Get name
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Cena
2014-05-04 21:09:44.758 AccessorDemo[1021:303] Cena


由上面的结果我们可以得出如下结论:

1.使用属性访问实例变量,需要向self发送消息(即调用accessor方法),从而有一个消息转发的过程。而直接访问实例变量则绕过了这一过程,无疑后者更快。

2.注意到属性有多种特性修饰,例如strong,weak,copy,retain,nonatomic。

在调用accessor方法时会根据其特性进行定制,例如对于copy特性的name,accessor方法类似于下面的形式:

- (void)setName:(NSString *)aName {
    NSLog(@"Set name");
    _name = [aName copy];
}

- (NSString *)name {
    NSLog(@"Get name");
    return [_name copy];
}

如果使用_name直接访问实例变量,那么上面的copy过程便会被绕过。这就绕过了为属性定义的所谓“内存管理语义”,这明显不好。

尤其如果开发者重写了属性的accessor方法,那么开发者额外定制的内容也得不到执行。


折中的方法是,访问实例变量时直接访问,设置实例变量值时调用属性的setter方法。这样既保证效率又保证了内存管理语义得到执行。


特殊情况

某些情况下,直接访问实例变量和使用属性的accessor方法访问只能二取其一。

下面列举三种情形:

1.不要在setter方法中调用setter方法或使用dot syntax

将上面的setter方法修改如下:

- (void)setName:(NSString *)aName {
    NSLog(@"Set name");
//    _name = [aName copy];
    self.name = aName;
}

跑一跑,控制台不停输出Set name,崩溃。

原因:在setter方法中调用setter方法会不断嵌套调用,最终导致程序崩溃。

所以自己重写属性setter方法时就不要犯这种低级错误了。

getter方法同理。


2.不要在init和dealloc方法中调用accessor方法

下面举一个例子。

我们写一个Wrestler的子类Cena,该类继承了属性name并重写了其setter方法,该方法会先检验名字后缀是否为Cena,否则抛出异常。

@interface Cena : Wrestler

- (instancetype)initWithName:(NSString *)aName;

- (void)wrestle;

@end

@implementation Cena
@synthesize name = _name;

- (instancetype)initWithName:(NSString *)aName {
    self = [super init];
    
    if (self) {
        NSLog(@"self.name = aName");
        self.name = aName;
    }
    
    return self;
}

- (void)wrestle {
    NSLog(@"I'm %@, U can't see me", self.name);
}

- (void)setName:(NSString *)aName {
    if (![aName hasSuffix:@"Cena"]) {
        [NSException raise:NSInvalidArgumentException format:@"last name must be Cena"];
    }
    
    _name = [aName copy];
}

@end
测试代码如下:

        Cena *cena = [[Cena alloc] initWithName:@"John Cena"];
        [cena wrestle];

无错运行。


但是某位老兄在父类Wrestler的init方法中将name初始化为空白字符串@"",代码如下:

- (instancetype)init {
    self = [super init];
    
    if (self) {
        NSLog(@"self.name = empty string");
        self.name = @"";
    }
    
    return self;
}

再跑一次,崩溃了。

原因:self.name = @"";调用子类中覆写的name的setter方法,空白字符串明显没有@"Cena"后缀,从而抛出异常。

解决方法,在Wrestler方法中直接访问实例变量,不要调用setter方法:

- (instancetype)init {
    self = [super init];
    
    if (self) {
        NSLog(@"self.name = empty string");
//        self.name = @"";
        _name = @"";
    }
    
    return self;
}

总结,绝不要在父类初始化中调用setter/getter方法,因为它们可能被子类重写,而子类又在它们身上添加了一些较为严苛的要求。

我想这应该可以回答这篇博客中提出的问题:不要在init和dealloc函数中使用accessor


3.在Lazy Initialization中必须通过getter方法访问属性

例如我们在Cena类中添加以下属性和方法:

@property (strong, nonatomic) NSNumber *chamCount;
- (void)showChampionCount;

- (NSNumber *)chamCount {
    if (!_chamCount) {
        _chamCount = @13;
    }
    
    return _chamCount;
}

- (void)showChampionCount {
    NSLog(@"Champion count = %d", [_chamCount integerValue]);
}

这里的chamCount属性的getter方法使用了Lazy Initialization,只有用到该属性才会初始化。

测试代码:

        Cena *cena = [[Cena alloc] initWithName:@"John Cena"];
        
        [cena showChampionCount];
输出和调试结果如下:

2014-05-04 22:04:51.722 AccessorDemo[1798:303] Champion count = 0
(lldb) po _chamCount
 nil

原因:直接访问_chamCount绕过了属性的getter方法,使其没有初始化。

解决方法,使用getter方法访问:

- (void)showChampionCount {
//    NSLog(@"Champion count = %d", [_chamCount integerValue]);
    NSLog(@"Champion count = %d", [self.chamCount integerValue]);
}

运行输出:

2014-05-04 22:07:16.475 AccessorDemo[1813:303] Champion count = 13



总结

1.在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。

2.在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。

3.使用Lazy Initialization配置的数据,应该通过属性来读取数据。

4.不要在setter/getter方法中调用setter/getter方法

5.如果非得用直接访问实例变量的方法,那么尽量保持其内存管理语义得到实施(如copy)。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值