第六条:理解”属性“这一概念
-
”属性“是Objective-C的一项特性,用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为各种实例变量。实例变量通过存取方法来访问,getter方法用来读取变量值,setter 方法用于写入变量值。
-
开发者可以令编译器自动编写与属性相关的存取方法,可以使用点语法来访问其中的数据。编译器会把点语法转换为对存取方法的调用,使用点语法的效果与直接调用存取方法相同。
EOCPerson *aPerson = [EOCPerson new];
aPerson.firstName = @"Bob"; // same as
[aPerson setFirstName:@"bob"];
NSString *lastName = aPerson.lastName; // same as
NSString *lastName = [aPerson lastName];
-
使用了属性,编译器就会自动编写访问这些属性所需的方法,编译器还会自动向类中添加适当类型的实例变量,并且在属性名前加下划线,以作为实例变量的名字。上面的代码中,会生成两个实例变量,名称分别为
_firstName
和_lastName
-
可以使用
@synthesize
语法指定实例变量的名字,建议使用编译器生成的即可。
@implementation EOCPerson
@synthesize firstName = myFirstName;
@synthesize lastName = myLastName;
@end
- 如果不想让编译器自动生成存取方法,可以自己实现。如果你只实现了其中一个存取方法,那么另外一个还是会由编译器自动生成。
- 还可以使用
@dynamic
关键字阻止编译器自动生成存取方法,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要创建存取方法。在编译时,即使编译器发现没有存取方法,也不会报错,编译器相信,这些方法运行期一定能找到。
@implementation EOCPerson
@dynamic firstName, lastName;
@end
- 如果没有自己实现存取方法,运行期编译器就找不到setter和getter方法而导致程序崩溃。
属性特质:属性可以拥有的特质分为四类
-
原子性:定义属性的时候,默认就是
atomic
的。 -
读/写权限:
-
readwrite(读写):属性拥有
setter
方法和getter
方法 -
readonly(只读):属性仅拥有
getter
方法
-
readwrite(读写):属性拥有
-
内存管理语义:属性用于封装数据,而数据要有具体的所有权语义。编译器在合成存取方法的时候,要根据此特质决定生成的代码。
-
assign:
setter
方法只会执行针对纯量类型(CGFloat、NSInteger等)的简单赋值。 -
strong:此特质表明该属性定义了一种拥有关系,这种属性在设置新值时,
setter
方法会先保留新值,并释放旧值,然后将新值设置上去。 -
weak:此特质表明该属性定义了一种非拥有关系,这种属性在设置新值时,
setter
方法既不保留新值,也不释放旧值。此特质通assign
类似,然而在属性所指的对象销毁时,属性值也会被清空,会指向nil
。 -
unsafe_unretain:此特质的语义和
assign
类似,但是它适用于对象类型,该特质表达非拥有关系,当目标对象销毁时,属性不会自动清空,不会指向nil
。 -
copy:此特质所表达的所属关系与
strong
类似。然而setter
方法并不保留新值,而是将其拷贝。当属性类型为NSString*
时,经常用此特质来保护其封装性。
@interface ViewController () @property (nonatomic, copy) NSString *str; // 使用 copy 修饰 @property (nonatomic, strong) NSString *str; // 使用 strong 修饰 @end // 首先,父类指针可以指向子类即NSString* 可以指向 NSMutableString*类型 // 其次,给self.str 赋值时候,本质是调用了 setter方法 NSMutableString *mStr = [NSMutableString stringWithFormat:@"abc"]; self.str = mStr; NSLog(@"mstr - %@", mStr); // mstr - abc NSLog(@"str - %@", self.str); // str - abc [mStr appendString:@"123"]; NSLog(@"mstr - %@", mStr); // mstr - abc123 NSLog(@"str - %@", self.str); // 如果使用copy修饰属性 打印结果是 str - abc,因为copy修饰在setter方法时会拷贝一个副本出来 // 如果使用strong修饰属性 结果是 str - abc123
-
assign:
-
方法名:定义属性时可以指定存取方法的方法名:
-
getter={name}:指定
getter
方法的方法名。如果某个属性时布尔类型,想为其添加is
前缀,那么就可以用这个方法指定。 -
setter={name}:指定
setter
方法的方法名,通常不这么干。
-
getter={name}:指定
-
如果自己实现这些存取方法,那么应该保证在实现的时候,具备相关属性所声明的特质。比如,将某个属性设置为
copy
,那么就应该在setter
方法中拷贝相关对象。 -
如果想在其他地方设置属性值,同样要遵守属性定义中所宣称的语义。
@interface EOCPerson : NSObject
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end
// EOCPerosn.m
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
if (self = [super init]) {
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
第七条:在对象内部尽量直接访问实例变量
在对象之外访问实例变量的时候,总是应该通过属性来访问。除了几种特殊情况外,在对象的内部,读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来访问。
看下面这个例子:
// EOCPerson.h
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
- (NSString *)fullName;
- (void)setFullName:(NSString *)fullName;
@end
// EOCPerson.m 使用点语法访问实例变量实现
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
- (void)setFullName:(NSString *)fullName {
NSArray *components = [fullName componentsSeparatedByString:@" "];
self.firstName = [components objectAtIndex:0];
self.lastName = [components objectAtIndex:1];
}
// EOCPerson.m 使用直接访问实例变量方式实现
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}
- (void)setFullName:(NSString *)fullName {
NSArray *components = [fullName componentsSeparatedByString:@" "];
_firstName = [components objectAtIndex:0];
_lastName = [components objectAtIndex:1];
}
这两种写法有几个区别:
- 由于不经过Objective-C的方法派发步骤,所以直接访问实例变量的速度比较快。编译器所生成的代码会直接访问保存对象实例变量的那块内存。
- 直接访问实例变量,不会调用其
setter
方法,这样就绕过了属性定义时候内存管理语义。比如,ARC下声明了一个copy
属性,那么因为直接访问实例变量,不会进行拷贝操作。 - 如果直接访问实例变量,不会触发KVO通知。
- 通过属性来访问实例变量,有助于排查错误,可以在
setter
、getter
方法中加断点,进行断点调试。
针对以上的问题,出现了一种折中的方案:在写入实例变量时,通过setter
方法来做,而在读取实例变量时,则直接访问实例变量。此方法,既能提高读取的速度,还能控制对属性的写入,还能确保属性内存管理语义贯彻。
- 选用这种方案,还需要注意几个问题:
-
在初始化方法中,应该直接访问实例变量,因为子类可能会重写
setter
方法,如果在父类的初始化方法中使用了setter
方法,当创建子类对象,父类初始化的时候,就会调用子类的重写的setter
方法,从而产生报错。 -
如果待初始化的变量在父类中,而我们又无法在子类直接访问实例变量,此时就需要调用
setter
方法了。 -
懒加载,必须通过
getter
方法来调用,否则无法获取到真正的值,因为对象根本就没有被初始化。
-
在初始化方法中,应该直接访问实例变量,因为子类可能会重写
网友评论