本文大纲
1.Apple Adopting Modern Object-C翻译
2.@property基本用法
3.@property修饰符详解
4.@property进阶话题:深入代码理解
Apple在Adopting Modern Objective-C一文中介绍了现代化OC的写法,其中就介绍尽量使用@property定义类的属性,先来看看苹果是怎么介绍property的。
Apple Official Property Introduction
Objection-C的属性(property)是通过用@property定义的公有或私有的方法。例如:
@property(readonly,getter=isBlue) BOOL blue;
属性捕获来了对象的状态。他们反应了对象的固有属性(intrinsic attributes)以及对象与其他对象之间的关系。属性(propetry)提供了一种安全、便捷的方式来与这些属性(attribute)交互,而不需要手动编写一系列的访问方法,如果需要的话可以自定义getter和setter方法来覆盖编译器自动生成的相关方法。
尽量多使用属性(property)而不是实例变量(attribute),因为属性(property)相比实例变量有很多的好处:
1.自动合成getter和setter方法。当声明一个属性(property)的时候编译器默认情况下会自动生成getter和setter方法
2.更好的声明一组方法。因为访问方法的命名约定,可以很清晰的看出getter和setter的用处。
3.属性(property)关键词能够传递出相关行为的额外信息。属性提供了一些可能会使用的特性来进行声明,包括assign(vs copy),weak,strong,atomic(vs nonatomic),readwrite,readonly等。
属性方法遵守一个简单的命名约定。getter的名字与属性名相同(如:属性名为date,则getter的名字也为date),setter的名字则是属性名字上set前缀并采用驼峰命名规则(如:属性名为date,则setter的名字为setDate)。布尔类型的属性还可以定义一个以is开头的getter方法,如:
@property (readonly,getter=isBlue) BOOL blue;
如果按照上面的方法声明则以下所有访问方式都正确:
if (color.blue){}
if(color.isBlue){}
if([color isBlue]){}
当决定什么东西可以作为一个属性的时候,需要注意以下这些不属于属性:
* init方法
* copy和mutableCopy
* 类工厂方法
* 开启某项操作并返回一个BOOL结果的方法
* 明确的改变了一个getter的内部状态的副作用方法
除此之外,在你的代码中使用属性特征的时候请考虑一下规则:
* 一个可读写(read/write)的属性有两个访问方法。setter方法是有一个参数的无返回值方法,getter方法是没有参数的且有一个返回值的方法,返回值类型与属性声明的类型一致。如果将这组方法转换成一个属性,就可以用readwrite关键字来标记它(默认即为readwrite,可不写)。
* 一个只读(read-only)的属性只有一个访问方法。即getter方法,它不接受任何参数,并且返回一个值。如果将这个方法转换成一个属性,就可以用readonly关键自标记它。
* getter方法应当是幂等(idempotent)的(如果一个getter方法被调用两次,那么第二次调用时返回的结果应该和第一次调用时返回的结果相同)。raner,如果一个getter方法每次调用时,是被用于计算结果,这是可以接受的。
如何适配
识别出一组可以被转换成一个属性的方法,如这些方法:
1 -(NSColor*)backgroundColor;
2 -(void)setBackgroundColor:(NSColor*)color;
用@property语法和适当的关键字将它们定义成一个属性:
@property (copy) NSColor *backgroundColor;
有关属性关键词和其他注意事项,可以阅读Encapsulating Data
或者,你也可以使用Xcode中的modern Object-C转换器来自动转换你的代码。参考Refacroring Your Code Using Xcode。
@property基本用法
手工创建getter与setter
@interface Person :NSObject
{NSString *_name;
NSUInteger _age;
}
-(void)setName:(NSString*)name;
_(NSString*)name;
-(void)setAge:(NSUInteger)age;
-(NSUInteger)age;
@end
@implementation Person
-(void)setName:(NSString*)name{
_name = [name copy];
}
-(NSString*)name{
return_name;
}
-(void)setAge:(NSUInteger)age{
_age = age;
}
-(NSUInteger)age{
return_age;
}
@end
上述代码就是手动创建变量的getter和setter的实现,getter和setter本质就是符合一定命名规范的实例方法。
具体使用方法如下
int main(int argc,const char * argv[]){
@autoreasepool{
Person *p = [[Person alloc] init];
//用函数调用name的setter
[p setName:@"meishaonv"];
//用函数调用age的setter
[p setAge:22];
//用函数调用name和age的getter,输出 meishaonv 22
NSLog(@"%@ @ld",[p name],[p age]);
}
return 0;
}
通过调用方式可以看出,setter和getter本质就是实例方法,可以通过函数调用的方式来使用。
为了方便使用,Object-C允许使用点语法来访问getter和setter
int main(int argc,const char *argv[]){
@autoreleasepool{
Person *p =[ [Person alloc] init];
//使用点语法访问name的setter
p.name = @“meishaonv”;
//使用点语法访问age的setter
p.age = 22;
//使用点语法访问name和age的getter
NSLog(@“%@ %@”,p.name, p.age);
}
return 0;
}
使用点语法访问的方式本质还是调用了我们手动创建的setter和getter。
当有很多变量需要设置时,这样手工创建setter和getter方式难免很繁琐,因此合成存取方法就诞生了。
合成存取方法
@interface Person : NSObject
@property (nonatomic,copy) NSString* name;
@property (nonatomic,assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
在声明一个属性(property)的时候尽量使用Foundation框架的数据类型,如整型使用NSIngeter或NSUInteger表示,时间间隔的浮点类型使用NSTimeInterval表示,这样代表数据类型更统一。
上面的代码使用@property声明两个属性name和age并为其设置了一些指示符(nonatomic,copy,assign等,下文会详细介绍)。
@systhesize表示为这两个属性自动生成名为name和_age的底层实例变量,并自动生成相关的getter和setter,也可以不用编写,编译器默认会生成‘属性名’的实例变量以及相关的getter和setter。
这里所说的编译器自动生成的实例变量就如同我们在上面手动创建的setter和getter时声明的变量_name和_age。也就是说编译器会在编译时自动生成并使用_name和_age这两个变量来存储这两个属性,跟name和age没什么关系了,只是我们在上层使用这两个属性的时候可以用name和age的点语法来访问getter和setter。如果不想使用这两个名字用于底层的存储也可以任意命名,但最好按照官方的命名规则来命名。
也可以自定义getter和setter方法来覆盖编译器默认生成的方法,就如同手动创建getter和setter一样。
@interface Person:NSObject
@property(nonatomic,copy) NSString* name;
@prpperty(nonatomic,assign)NSUInteger age
@end
@implementation Person
//编译器会帮我们自动生成_name和_age这两个实例变量,下面代码就可以正常使用这两个变量了
@synthesize name = _name;
@systhesize age = _age;
-(void)setName:(NSString*)name{
//必须使用_name来赋值,使用self.name来设置值时编译器会自动转为调用函数,会导致无限递归
//使用_name则是直接访问底层的存储属性,不会调用该方法来赋值
//这里使用copy是为了防止NSMutableString多态性
_name = [name copy];
}
-(NSString*name{
//必须使用_name来访问属性值,使用self.name来访问值时编译器会自动转换为调用该函数,会造成无限递归
return _name;
}
@end
使用自定义的getter和setter一般是用来实现懒加载(lazy load),在很多情况下很常用,比如:创建一个比较大的而又不一定会使用的对象,可以按照如下方法编写。
@property (nonatomic,strong)CustomObject *customObject;
@synthesize customObject = _customObject;
-(CustomObject*) customObject{
if(_customObject==nil){
//初始化操作。会调用setter方法
self.customObject = [[CustomObject alloc] init];
//如果按照如下方法编写不会调用setter方法,如果自定义setter方法需要完成一些事情建议使用self.customObject的方式来设置
//_customObject =[[CustomObject alloc] init];
}
return _customObject;
}
@property指示符号
在声明属性的时候一般会带上几个指示符,常用指示符有
* atomic nonatomic
* readwrite readonly
* assign
* strong
* weak
* copy
* unsafe_unretained
* retain
还可以设置getter和setter对其重新命名,这里不再赘述。
atomic/nonatomic
指定合成存取方法是否为原子操作,可以理解为是否线程安全,但在iOS上即使用atomic也不一定是线程安全的,要确保线程安全需要使用锁机制,超过本文的讲解范围,可以自行查阅。
可以发现几乎所有代码的属性设置都会使用nonatomic,这样能够提高访问性能,在iOS中使用锁机制的开销较大,会损耗性能。
readwrite/readonly
readwrite是编译器的默认选项,表示自动生成getter和setter,如果需要getter和seter,不写即可。
redonly表示只合成getter而不合成setter。
assign weak unsafe_unretained
assign表示对属性只进行简单的赋值操作,不更改所赋的新值的引用计数,也不会改变酒值的引用计数,常用于标量类型,如NSInteger,NSUInteger,CGFloat,NStimeInterval等。
assign也可以修饰对象如NSString等类型对象,上面说过使用assign修饰不会更改所赋的新值的引用计数,也不改变旧值的引用计数,如果当所赋的新值引用计数为0对象被销毁时,属性并不知道,编译器不会将该属性置为nil,指针仍旧执行之前被销毁的内存,这时访问该属性会产生野指针错误并崩溃,因此使用assign修饰的类型一定要为标量类型。
@interface Person :NSObject
@property (nonatomic,assign) NSString* name;
@property (nonatomic,assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc,const char *argv[]){
@autoreleasepoll {
Person *p = [[Person allco] init];
//这里使用NSMutableString而不是使用NSString是因为NSString会缓存字符串,后面置空的时候实际没有被销毁
NSMutableString *s = [[NSMutableString alloc] initWithString:@"meinv xiao"];
//设置p.name不会增加s的引用计数,只是单纯将s指向的地址赋给p.name
p.name = s;
//输出两个变量的内存地址,可以看出是一致的
NSLog(@"%p %p",p.name,s);
//这里可以正常访问name
NSLog(@"%@ %ld",p.name,p.age);
//将上述字符串置空,引用计数为0,对象被销毁
s = nil;
//查看其地址时仍然可以访问到,表示其仍然指向那一块内存
NSLog(@"%@",p.name);
//访问访问内容时发生野指针,程序崩溃,因为对象已经被销毁
NSLog(@"%@,%ld",p.name,p.age);
}
return 0;
}
使用weak修饰的时候同样不会增加所赋的新值的引用计数,也不会减少旧值的引用计数,但当该指针被销毁时,weak修饰的属性会被自动赋值为nil,这样就可以避免野指针错误。
使用unsafe_unretained修饰时效果与assign相同,不会增加引用计数,当所赋的值被销毁时不会被置为nil,可能会发生野指针的错误。unsafe_unretained与assign的区别在于,unsafe_unretained只能修饰对象,不能修饰标量类型,而assign两者均可以修饰。
为了防止多态的影响,对NSString进行修饰时一般使用copy。
下面会对weak、unsafe_unretained和copy进行详细介绍。
strong weak
strong表示属性所赋的值持有强引用,表示一种“拥有关系”(owning relationship),会先保留新值即增加新值的引用计数,然后在释放旧值即减少旧值的引用计数。只能修饰对象。如果对一些对象需要保持强引用则使用strong。
weak表示对所赋的值对象持有弱引用表示一种“非拥有关系”(nonowing relationship),对新值不会增加引用计数,也不会减少旧值的引用计数。所赋的值在引用计数为0被销毁后,weak修饰的属性会被自动置为nil,能够有效防止野指针错误。
weak常用在修饰delegate等防止循环引用的场景。
copy
copy修饰的属性会在内存里拷贝一份对象,两个指针指向不同的内存地址。
一般用来修饰有对应可变类型子类的对象。
如:NSString/NSMutableString,NSArray/NSMutableArrary,NSDictionary/NSMutableDictionary等。
为确保这些不可变对象因为可变子类对象影响,需要copy一份备份,如果不使用copy修饰,使用strong或assign等修饰,则会因为多态导致属性值被修改。
这里的copy还牵扯到NSCopying和NSMutableCopying协议,在下面会有简单介绍。
@interface Person : NSObject
//使用strong修饰NSString
@property (nonatomic,strong) NSStrign* name;
@property (nonatomic,strong) NSUInteger age;
@end
@implementation Person
@systhesize name = _name;
@systhesize age = _age;
@end
int main(int argc,const char *argv[]){
@autoreleasepool {
Person *p = [[Person alloc] init];
NSMutableString *s =[[NSMutableString alloc] initWithString:@"meinvxiao"];
//将可变字符串赋值给p.name
p.name = s;
//输出的地址和内容均一致
NSLog(@"%p %p %@ %@",p.name,s p.name,s);
//修改可变字符串s
[s appendString:@"is a good guy"];
//再次输出p.name被影响
NSLog(@"%p %p %@ %@",p.name,s,p.name,s);
}
return 0;
}
copy还被用来修饰block,在ARC环境下编译器默认会用copy修饰,一般情况下在block需要捕获外界数据时该block就会被分配在堆区,但是在MRC环境下,由于手动管理引用计数,block一般被分配在栈区,需要copy到堆区来防止野指针错误。由于牵扯block相关知识,有兴趣可以看此文章
对于可变对象类型,如NSMutableString、NSMutableArray等则不可以使用copy修饰,因Foundation框架提供的这些类都实现了NSCppying协议,使用copy方法返回的都是不可变对象,如果使用copy修饰符在对可变对象赋值时则会获得一个不可变对象,接下来如果对这个对象进行可变对象的操作则会产生异常,因为oc没有提供mutableCopy修饰符,对于可变对象使用strong修饰符即可,具体例子如下:
@interface Person : NSObject
//使用strong修饰NSString
@property (nonatomic,copy) NSMutableString* name;
@property (nonatomic,strong) NSUInteger age;
@end
@implementation Person
@systhesize name = _name;
@systhesize age = _age;
@end
int main(int argc,const char *argv[]){
@autoreleasepool {
Person *p = [[Person alloc] init];
NSMutableString *s =[[NSMutableString alloc] initWithString:@"meinvxiao"];
//将可变字符串赋值给p.name
p.name = s;
//输出的地址不一致,内容均一致
NSLog(@"%p %p %@ %@",p.name,s p.name,s);
//修改可变字符串p.name,此时抛出异常
[p.name appendString:@"is a good guy"];
}
return 0;
}
上面的例子使用copy修饰可变对象,在进行赋值的时候会通过copy方法获得一个不可变对象,因此p.name的地址和s的地址不同,而p.name运行时类型为NSString,调用appendString:方法会抛出一场。
所以,针对不可变对象使用copy修饰,针对不可变对象使用strong修饰。
unsafe_unretained
使用unsafe_unretained修饰时效果与assign相同,不会增加新值的引用计数,也不会减少旧值的引用计数(unretained,当所赋的值被销毁时不会被设置为nil,肯能会发生野指针错误(uisafe)。unsafe_unretained与assign的区别在于,unsafe_unretained只能修饰对象,不能修饰标量类型,而assign两者均可修饰。
retain
在ARC环境下使用较少,在MRC下使用效果与strong一致。
copy的题外话
有时候我们需要copy一个对象,或者mutableCopy一个对象,这时需要遵守NScopying和NSMutableCopying协议,来实现copyWithzone:和mutableCopyWithZone:两个方法,而不是重写copy和mutableCopy两个方法。
Foundation框架中的很多数据类型已经帮我们实现了上述两个方法,因此我们可以使用copy和mutableCopy方法来复制一个对象,两者区别在于copy的返回值仍为不可变对象,mutableCopy的返回值为可变对象。
copy mutableCopy
NS* 前拷贝,只拷贝指针,地址相同
NSMutable* 单层深拷贝,拷贝内容,地址不同
由上述表格可以看出,对于不可变类型,使用copy方法时是前拷贝,只拷贝指针,因为内容是不会变化的。使用mutableCopy时由于返回可变对象,因此需要一份拷贝,供其他对象使用。对于可变类型,不管copy还是mutableCopy均会进行深拷贝,所指向指针不同。
上面介绍copy修饰符的时候讲过,在修饰NSString这样的不可变对象的时候使用copy,但其实当给对象赋一个NSString时仍旧只赋值了指针而不是拷贝内容,原因同上。
@interface Person : NSObject
//使用strong修饰NSString
@property (nonatomic,copy) NSString* name;
@property (nonatomic,strong) NSUInteger age;
@end
@implementation Person
@systhesize name = _name;
@systhesize age = _age;
@end
int main(int argc,const char *argv[]){
@autoreleasepool {
Person *p = [[Person alloc] init];
NSMutableString *s =@"xiaomeinv";
//将可变字符串赋值给p.name
p.name = s;
//输出的地址一致,不可变对象copy是前拷贝
NSLog(@"%p %p %@ %@",p.name,s p.name,s);
}
return 0;
}