《Effective Objective-C 2.0》读书笔记——对象、消息、运行期


第二章:对象、消息、运行期

对象之间能够关联与交互,这是面向对象语言的重要特征。本章讲述这些特,并深入研究代码在运行期的行为。
Runtime就是为程序运行起来后提供相关支持的代码,它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。

第6条:理解“属性”这一概念

在对象的接口定义中,可以使用属性来访问封装在对象里的数据📊。可以把属性当作一种简称:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的量。
@property来定义属性:

@interface EOCPerson : NSObject

@property NSString* firstName;
@property NSString* lastName;

@end

对于上述类的使用者,等效下面👇这种写法:

@interface EOCPerson : NSObject

- (NSString *)firstName;
- (void)setFirstName:(NSString * _Nonnull)firstName;
- (NSString *)lastName;
- (void)setLastName:(NSString * _Nonnull)lastName;

@end

可使用“点语法”(dot syntax)访问属性,编译器会把“点语法”转换为对存取方法的调用。因此,使用“点语法”和直接调用存取方法之间没有丝毫差别,两者等效:

EOCPerson* aPerson = [EOCPerson new];
        
aPerson.firstName = @"Jacky";  // Same as
[aPerson setFirstName: @"Jacky"];
        
NSString* lastName = aPerson.lastName;  // Same as
NSString* lastName = [aPerson lastName];

从上例可以看到,如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis),由编译器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。
除了生成方法代码外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前加下划线_,以此作为实例变量的名字。比如前例中,会生成_firstName和_lastName两个实例变量,可通过@synthesize语法来指定实例变量的名字:

@implementation EOCPerson

@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;

@end

通过@synthesize语法会将生成的实例变量命名为_myFirstName与_myLastName,而不再使用默认的名字。

使用@dynamic关键字,会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法,即告诉编译器你要自己去实现。即使未手动实现存取方法,编译器也不会报错,它相信这些方法能在运行期找到。

@dynamic firstName, lastName;

属性的各种特质(attribute)设定也会影响编译器所生成的存取方法。下面这个属性指定了三项特质:

@property (nonatomic, readwrite, copy)NSString* firstName;

属性可以拥有的特质分为四类:

原子性(atomic / nonatomic)

在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity),编译器会自动生成互斥锁,对 setter 和 getter 方法进行加锁,可以保证属性的赋值和取值的原子性操作是线程安全的,但不包括操作和访问
比如说atomic修饰的是一个数组的话,那么我们对数组进行赋值和取值是可以保证线程安全的。但是如果我们对数组进行操作,比如说给数组添加对象或者移除对象,是不在atomic的负责范围之内的,所以给被atomic修饰的数组添加对象或者移除对象是没办法保证线程安全的。

在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的”,或者说该操作具备“原子性”。

如果属性具备nonatomic特质,则不使用同步锁。一般属性都用 nonatomic 进行修饰,因为 atomic 非常耗时。

atomic与nonatomic的区别是什么呢? 具备atomic特质的getter方法会通过锁定机制来确保其操作的原子性。
也就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值。
若是不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在改写某属性值时,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来。发生这种情况,线程读到的属性值可能不对。

读 / 写权限

  • 具备readwrite(读写)特质的属性同时拥有setter和getter方法的声明和实现。
  • 具备readonly(只读)特质的属性仅有getter获取方法。

内存管理语义

属性用于封装数据,而数据则要有“具体的所有权语义”(concrete ownership semantic)。下面这组特质仅会影响setter方法。
例如,用setter设定一个新值时,它是应该“保留”(retain)此值呢,还是只将其赋给底层实例变量就好?
如果自己编写存取方法,那么就必须同有关属性所具备的特质相符。

属性关键字 注意点
assign:setter方法只会执行针对基本数据类型(纯量类型,scalar type,例如BOOL、CGFloat或NSInteger等)的简单赋值操作。 修饰对象类型时,不增加其引用计数,且会产生“悬垂指针”,意味着当被修饰的对象被释放后,指针仍指向原对象地址,这时如果继续通过该指针访问原对象的话,就可能导致程序崩溃 。
strong:setter方法会先保留新值,并释放旧值,再将新值设置上去。 ARC下才可使用;修饰强引用,将指针原来指向的旧对象释放掉,然后指向新对象,同时将新对象的引用计数+1。
weak:setter方法既不保留新值,也不释放旧值。 ARC下才可使用;修饰弱引用,不增加对象引用计数,主要可用于避免循环引用;属性所指对象在被释放后,会自动将指针置为nil,即不会产生悬垂指针。
copy:setter方法不保留新值,而是将其拷贝,并释放旧值。 用于NSString、block等类型。比如NSString*,此特质保护其封装性,因为传递给setter方法的新值有可能指向一个NSMutableString类的实例,此时若是不拷贝本身不可变的字符串,那么在设置完属性后,字符串的值就可能会在对象不知情的情况下遭人更改。
retain:原理同strong,但在修饰block时,strong相当于copy,而retain相当于assign。 MRC下使用,ARC下常使用strong
unsafe_unretained MRC下经常使用,原理同weak,区别就在于unsafe_unretained会产生悬垂指针

方法名

可通过如下特质来指定存取方法的方法名:

  • getter=< name >,指定“获取方法”的方法名。
  • setter=< name >,指定“设置方法”的方法名。

在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
注意:尽量不要在初始化方法和dealloc方法里调用setter / getter方法(🚩详见第7条)。

@property (nonatomic, copy)NSString* name;

- (instancetype)initWithName: (NSString *)name {
   
    self = [super init];
    if (self) {
   
        _name = [name copy];
    }
    return self;
}

若是自己实现存取方法,也应保证其具备相关属性所声明的性质。

第7条:在对象内部尽量直接访问实例变量

直接访问实例变量和通过属性访问的区别:

  • 由于不经过OC的“方法派发”(method dispatch,🚩参见第11条)步骤,直接访问实例变量的速度比较快,编译器所生成的代码就直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量,不会调用其setter方法,这就绕过了为相关属性所定义的“内存管理语义”。比如,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新对象释放旧对象。
  • 直接访问实例变量不会触发“键值观测”(Key-Value Observing,KVO)通知,因为KVO时通过在运行时生成派生类并重写setter方法以达到通知所有观察者的目的。这样做是否会产生问题,取决于具体的对象行为。
  • 通过属性来访问有助于排查与之相关的错误,因为可以在setter和getter方法中设置断点来调试。

有一种合理的折中方案:在写入实例变量时,通过setter来做;在读取实例变量时,则直接访问。
此办法既能提高读取操作的速度,又能控制对属性的写入操作。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关属性的“内存管理语义”得以贯彻。
选用这种方案时需注意以下问题:

  1. 初始化方法中不建议使用存取方法,应该直接访问实例变量。因为子类可能会重写setter方法。

    假设EOCPerson有一个子类,叫做EOCSmithPerson,这个子类专门表示那些姓“Smith”的人。该子类可能会覆写lastName属性所对应的setter方法:

    - (void)setLastName: (NSString *)lastName {
          
          if (![lastName isEqualToString: @"Smith"]) {
          
              [NSException raise: NSInvalidArgumentException format: @"Last name must be Smith"];
              self.lastName = lastName;
          }
    }
    

    在父类EOCPerson的默认初始化方法中,可能会将姓氏设为空字符串self.lastName = @"";。此时若是通过setter方法来设置字符串,那么调用的将会是子类的setter方法,导致抛出异常。
    为什么会是调用子类的setter方法?因为在子类中调用[super init]先初始化父类的东西,根据super的原理,是从父类开始查找方法的实现,而消息接受者还是子类。也就是说在父类的init方法中调用[self setLastName]中self是子类对象,而又由于子类重写了该方法,故调用子类的,不会调用父类的。

  2. 但有些情况下必须在初始化方法中调用存取方法:
    • 待初始化的实例变量声明在父类,此时我们就无法在子类中直接访问此实例变量,所以就只能调用setter方法:父类的初始化方法中直接访问实例变量,子类的初始化方法中通过 setter 方法访问。
    • 属性是懒加载(lazy initialization)的,就必须通过getter来访问,否则实例变量就永远不会初始化:
      - (EOCBrain *)brain {
             
          if (!_brain) {
             
              _brain = [Brain new];
          }
          return _brain;
      }
      
      若没有调用getter方法就直接访问实例变量,则会看到尚未设置好的brain。

第8条:理解“对象等同性”这一概念

==操作符比较的是两个指针本身,而不是其所指的对象,所以有时候结果并不是我们想要的。应该使用NSObject协议中声明的isEqual:方法来判断两个对象的等同性。
一般来说,两个类型不同的对象总是不相等的,因为某些对象提供了特殊的“等同性判断方法”(equality-checking method),如果已经知道两个受测对象都属于同一个类,那么就可以使用此方法,以NSString为例:

NSString* foo = @"Badger 123";
NSString* bar = [NSString stringWithFormat: @"Badger %i", 123];

BOOL equalA = (foo == bar);  //NO
BOOL equalB = [foo isEqual: bar];  //YES
BOOL equalC = [foo isEqualToString: bar];  //YES

调用isEqualToString:方法比调用isEqual:方法要快,因为后者还要执行额外的步骤,因为它不知道受测(比较)对象的类型。

NSObject协议中定义了两个判断同等性的关键方法:

请添加图片描述

这两个方法的默认实现是:当且仅当其指针值(内存地址)完全相等时,这两个对象才相等。
若想正确重写这两个方法,实现自定义对象的“等同性判定”,就必须遵守约定(contract):
如果isEqual:方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,其isEqual:方法未必会认为两者相等。
举例如下:

重写isEqual:

@interface EOCPerson : NSObject
@property (nonatomic, copy)NSString* firstName;
@property (nonatomic, copy)NSString* lastName;
@property (nonatomic, assign)NSUInteger age;
@end

- (BOOL)isEqual:(id)object {
   
    //指针本身是一个,说明受测的实例对象相等
    if (self == object) return YES;
    
    //判断是否属于同一类
    if ([self class] != [object class]) return NO;
    /*有时我们可能认为,一个实例可以与其子类实例相等,
      在继承体系中,判断同等性时,经常遭遇此问题
    */
    
    //检测属性是否相等
    EOCPerson* otherPerson = (EOCPerson *)object;
    if ([_firstName isEqualToString: otherPerson.firstName
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值