前语
在日常的iOS开发中,只要是运用Objective-C进行开发,就绕不开特点。而关于特点,其又拥有一系列的特性。据本人的经验以及在工作中的观察发现,iOS研制的新人对如何给一个特点设置适宜的特性,缺少满足的认知。于是,撰写本文,旨在帮助iOS研制的新人学习Objective-C特点的特性。
为了保证本文中文的翻译能一一对应上官方术语,这儿将本文呈现的术语做成表格,方便读者对照:
中文 | 英文 |
---|---|
特点 | property |
特性 | attribute |
存取器 | accessor |
本文环境:
- ARC (为了不影响新人对这一块的学习,全文不会呈现任何MRC的特性)
- Xcode 11.0 +
本文受众:
- iOS研制的新人 (需求有必定的Objective-C根底)
- 对Objective-C特点的特性缺少满足认知的同学
(本文解说的难度较低,仅会走马观花般解说一些粗浅的原理)
简略认识特点(property)
@interface FooClass : NSObject
@property id foo;
@end
如上所示,咱们知道,当咱们在一个类里边,简略声明一个特点时,编译器实际上帮咱们干了两件事:
- 生成实例变量
- 生成getter和setter办法
即,等价于如下界说:
@interface FooClass : NSObject {
@private id _foo;
}
- (id)foo;
- (void)setFoo:(id)foo;
@end
而特点中的特性,便是告知编译器,如何生成实例变量和getter/setter办法。
通常情况下,咱们会运用点办法来读写特点:
FooClass *foo = [FooClass new];
foo.foo = @"foo";
id obj = foo.foo;
实际上,咱们能够将其了解为getter/setter办法的“语法糖”,即咱们能够了解为编译器在预编译阶段会将点语法进行展开成getter/setter办法:
FooClass *foo = [FooClass new];
[foo setFoo:@"foo"];
id obj = [foo foo];
那什么时分会议开成getter办法,什么时分又会议开成setter办法呢?这儿能够记住一个诀窍:特点点办法的调用在赋值符号(等号,=)左边的场景会议开成setter办法,其他场景一律展开成getter办法。
特点中的特性(attribute)
在Objective-C项目中,咱们常常能看到以下写法:
@interface FooClass : NSObject
@property (nonatomic, strong) id obj;
@property (nonatomic, weak) id delegate;
@property (nonatomic, assign) BOOL flag;
@end
不难知道,特点标识符后面括号里边的关键字,即为描绘特点的特性。那么问题来了,这些特性关键字详细有什么含义?又有哪些特性?先按下不表,这儿咱们先给出界说特点的语法:
@property (attributes) type name;
然后,咱们给特性,划分出不同的类型:
如上图所示,特性一共有五大类型:
- 原子性 Atomicity
- 读写性 Writability
- 存储语义 Setter Semantics
- 存取器办法名 Accessor Method Names
- 空值性 Nullability
原子性 Atomicity
描绘特点原子性的特性一共有两个关键字:
-
atomic
表明这个特点的getter/setter办法是原子性的,即在多线程下拜访该特点对应的getter/setter办法是原子性的(即保证多线程调用getter/setter办法的安全) -
nonatomic
表明这个特点的getter/setter办法是非原子性的
初学者关于这两者的概念或许有点绕,这儿咱们举个比如:
@interface FooClass : NSObject
@property (atomic) id fooA;
@property (nonatomic) id fooN;
@end
在这儿比如中,咱们别离界说了一个atomic
和nonatomic
的特点。上面解说过,特性是用来告知编译器如何生成实例变量和对应getter和setter办法的。咱们这儿做一次等价转化:
@interface FooClass : NSObject {
@private id _fooA;
@private id _fooN;
}
- (id)fooA;
- (void)setFooA:(id)fooA;
- (id)fooN;
- (void)setFooN:(id)fooN;
@end
@implementation FooClass
- (id)fooA {
@synchronized (self) {
return _fooA;
}
}
- (void)setFooA:(id)fooA {
@synchronized (self) {
return _fooA = fooA;
}
}
- (id)fooN {
return _fooN;
}
- (void)setFooN:(id)fooN {
_fooN = fooN;
}
@end
不难看出,atomic
特点便是在nonatomic
特点的getter/setter办法的根底上,做了个加锁(递归锁)的操作。这样能保证,同一时间只有一条线程能调用特点的getter/setter办法。那么问题来了,atomic
特点便是线程安全的吗?为什么在工程实践中,很少有特点的原子性特性为atomic?
首先,atomic
特点并不是线程安全的,或许说它只保证了getter/setter办法的“线程安全”。还是举个比如:
@interface FooClass : NSObject
@property (atomic) NSMutableArray *array;
@end
// foo thread
FooClass *foo = [FooClass new];
foo.array = [NSMutableArray array];
// a thread
[foo.array addObject:@"a"];
// b thread
[foo.array addObject:@"b"];
// multi-thread
[foo.array addObject:@"..."];
咱们在多线程场景中,往foo
目标的array
添加目标,此刻大概率会发生crash。尽管,array
特点是atomic
的,而且在上面这个比如中,咱们在调用array
特点的getter办法时,也保证了原子性,可是咱们调用添加目标的办法addObject:
时,addObject:
办法并不是线程安全的,所以只要一起有两条线程拜访了addObject:
办法,就大概率会发生crash。在咱们的工程实践中,咱们往往是想保证一系列的办法调用是线程安全的,而不是仅仅保证某个getter和setter办法是线程安全的。
其次,atomic
特点的getter/setter办法因为有加锁的操作,故功率远远比不上nonatomic
特点的getter/setter办法。所以在工程实践中,咱们往往倾向于去声明一个nonatomic
特点。
读写性 Writability
描绘特点读写性的特性一共有两个关键字:
-
readwrite
可读写 -
readonly
只读
同样的思路,编译器想完成特点是readonly
的,它只需求生成getter办法而不生成setter办法即可:
// Normal Statement
@interface FooClass : NSObject
@property (readwrite) id fooRW;
@property (readonly) id fooRO;
@end
// Equivalence Statement
@interface FooClass : NSObject {
@private id _fooRW;
@private id _fooRO;
}
- (id)fooRW;
- (void)setFooRW:(id)fooRW;
- (id)fooRO;
@end
存储语义 Setter Semantics
描绘特点存储语义的特性一共有四个关键字:
-
strong
强引证 -
weak
弱引证 -
assign
仅赋值 -
copy
拷贝
咱们知道,在ARC下,Objective-C目标指针存在强指针和弱指针,即:
__strong id strongPtr;
__weak id weakPtr;
而与此对应的,strong
和weak
特性,指的便是当声明一个Objective-C目标特点时,实例变量指针是强指针还是弱指针:
// Normal Statement
@interface FooClass : NSObject
@property (strong) id fooStrong;
@property (weak) id fooWeak;
@end
// Equivalence Statement
@interface FooClass : NSObject {
@private __strong id _fooStrong;
@private __weak id _fooWeak;
}
- (id)fooStrong;
- (void)setFooStrong:(id)fooStrong;
- (id)fooWeak;
- (void)setFooWeak:(id)fooWeak;
@end
不难看出,strong
和weak
特性也只能用来声明Objective-C目标特点,不然编译器将报错无法经过编译。
而除了Objective-C目标特点之外,咱们还能声明根底数据类型特点,此刻assign
特性就派上用场了:
// Normal Statement
@interface FooClass : NSObject
@property (assign) NSInteger fooInt;
@end
// Equivalence Statement
@interface FooClass : NSObject {
@private NSInteger _fooInt;
}
- (id)fooInt;
- (void)setFooInt:(NSInteger)fooInt;
@end
换而言之,assign
特性对引证计数不会有任何影响,它生成的实例变量类型的等同于根底数据类型。所以,assign
特性其实也能用来声明Objective-C目标特点,可是因为调用其对应的setter办法时,仅仅是对指针进行一次赋值,故极有或许原目标释放时,该特点对应的实例变量指针成为野指针,而当再次拜访指针的内容时,极易发生crash。(这一块涉及ARC与MRC的常识,如若无法了解能够视为assign
特性便是用来描绘根底数据类型的特点)
那问题来了copy
特性有啥用?其他特性用来描绘生成的实例变量的类型,似乎已经覆盖了一切的或许性了。其实,copy
特性生成的实例变量的类型与strong
特性一致,故其也只能用来描绘声明Objective-C目标特点。可是与strong
特性不同的的是,两者的setter办法不一样:
// Normal Statement
@interface FooClass : NSObject
@property (nonatomic, strong) id fooStrong;
@property (nonatomic, copy) id fooCopy;
@end
// Equivalence Statement
@interface FooClass : NSObject {
@private __strong id _fooStrong;
@private __strong id _fooCopy;
}
- (id)fooStrong;
- (void)setFooStrong:(id)fooStrong;
- (id)fooCopy;
- (void)setFooCopy:(id)fooCopy;
@end
@implementation FooClass
- (void)setFooStrong:(id)fooStrong {
_fooStrong = fooStrong;
}
- (void)setFooCopy:(id)fooCopy {
_fooCopy = [fooCopy copy];
}
@end
能够看到,copy
特性的setter办法是调用入参目标的copy
办法。这时,咱们就能知道,为什么工程中,NSString
、NSArray
、NSDictionary
、NSSet
等不可变的容器类特点习惯运用copy
特性,便是为了保证当setter办法传入的是一个可变容器类目标时,经过调用copy
办法将目标固化成不可变容器类:
@interface FooClass : NSObject
@property (copy) NSString *str;
@end
// some function
NSMutableString *str = [NSMutableString stringWithString:@"str"];
FooClass *foo = [FooClass new];
foo.str = str;
[str appendString:@"str"];
NSLog(@"%@", foo.str); // Output ``str``
存取器办法名 Accessor Method Names
(存取器或许有点不太好了解,实际上便是getter/setter办法)
描绘特点存取器办法名的特性一共有两个关键字:
-
getter
getter办法名 -
setter
setter办法名
咱们知道,当咱们简略声明一个特点时,编译器会主动生成getter/setter办法,命名规范为:
// property statement
@property type name;
// getter method
- (type)name;
// setter method
- (void)setName:(type)name;
可是,咱们有些时分需求去修正getter/setter办法的姓名,这个时分就能够用上getter和setter特性了:
// Normal Statement
@interface FooClass : NSObject
@property (getter=isFlag, setter=changeFlag:) BOOL flag;
@end
// Equivalence Statement
@interface FooClass : NSObject {
@private BOOL _flag;
}
- (BOOL)isFlag;
- (void)changeFlag:(BOOL)flag;
@end
(需求留意的是,setter特性后跟着的setter办法名要带上:)
通常,咱们不需求改动存取器办法名,如若需求改动,尽量遵守KVC的命名规范(Key-Value Coding Programming Guide)。
空值性 Nullability
描绘特点空值性的特性一共有四个(在兼容Swift上有重要意义):
-
null_unspecified
不确定是否为空 (的确是这个意思) -
nullable
可空 -
nonnull
不可为空 -
null_resettable
getter不为空,setter能够为空
null_unspecified
关于非Swift开发者来说难以了解,这儿能够直接视为等同nullable
。一起需求留意的是,根本数据类型是不具有空值性特性的,可是根本数据类型的指针的确能够拥有空值性特性。不过,Objective-C实例目标咱们也是用的指针类型,故其实能够以为只有指针类型才具有空值性特性。
照例,咱们给个示例:
// Normal Statement
@interface FooClass : NSObject
@property (null_unspecified) id fooUnspecified;
@property (nullable) id fooNull;
@property (nonnull) id fooNonNull;
@property (null_resettable) id fooResettable;
@end
// Equivalence Statement
@interface FooClass : NSObject {
@private id _fooUnspecified;
@private id _fooNull;
@private id _fooNonNull;
@private id _fooResettable;
}
- (id _Null_unspecified)fooUnspecified;
- (void)setFooUnspecified:(id _Null_unspecified)fooUnspecified;
- (id _Nullable)fooNull;
- (void)setFooNull:(id _Nullable)fooNull;
- (id _Nonnull)fooNonNull;
- (void)setFooNonNull:(id _Nonnull)fooNonNull;
- (id _Nonnull)fooResettable;
- (void)setFooResettable:(id _Nullable)fooResettable;
@end
实际上,不遵守空值性特性,调用setter办法传入不符合预期的目标时,一般也不会导致编译过错,可是或许会收获Clang的警告(往往实际工程项目中会打开warning as error的编译选项,此刻如若不遵守空值性特性就会编译不经过)
而咱们在项目中,往往能见到.h文件中有这么一对宏:
NS_ASSUME_NONNULL_BEGIN
@interface FooClass : NSObject
// balabala
@property (nonatomic) id foo;
// balabala
@end
NS_ASSUME_NONNULL_END
这儿不对此做过多的解说,只需求知道,在这对宏之间没有显式声明空值性特性的特点,空值性特性都为nonnull
。
至此,一切Objective-C特点的特性都解说完了。
默许特性
关于没有显式地写特性的特点,编译器会给他们附上默许的特性:
@interface FooClass : NSObject
@property (
atomic,
readwrite,
strong,
getter=nsObj, setter=setNsObj:,
null_unspecified
) id nsObj;
@property (
atomic,
readwrite,
assign,
getter=cStruct, setter=setCStruct:,
) NSInteger cStruct;
@end
特性间的关系
往往,咱们在声明一个特点时,顺便的特性就不止一个。而往往并不是什么特性都能附加到特点上的。
共存
在同一次特点声明中,能够声明不同类型的特性。
互斥
- 同为原子性、读写性、存储语义或空值性类型的特性间互斥,即在一次特点声明中,已经声明了一个类型的特性后,无法再次声明相同类型的特性。
- 当一个类的特点的读写性特性为
readonly
时,允许在类扩展(extension)中再次声明特点,并显式将读写性特性声明为readwrite
。 - 当一个特点的读写性特性为
readonly
时,在同一次特点声明中,存取器办法名setter
特性会失效,且空值性的null_resettable
特性等同于nullable
特性。 - 当一个特点的存储语义特性为
weak
时,空值性特性不能声明为nonnull
。
重写存取器
在实际工程中,重写getter/setter办法的现象很常见,可是如何正确的重写getter/setter办法却缺少满足的认知。因为重写getter/setter办法,往往会破坏特点所遵守的特性,当调用方对详细完成不甚了解时,往往会陷入重写者制作的坑中。这儿给出常见的重写特点getter/setter办法的比如:
@interface FooClass : NSObject
@property (atomic) id fooAtomic;
@property (nonatomic, nonnull) id fooNonnull;
@property (nonatomic, copy) id fooCopy;
@property (nonatomic, weak) id fooWeak;
@property (nonatomic, setter=isFlag) BOOL flag;
@end
@implementation FooClass
// fooAtomic getter&setter
- (id)fooAtomic {
@synchronized (self) {
return _fooAtomic;
}
}
- (void)setFooAtomic:(id)fooAtomic {
@synchronized (self) {
_fooAtomic = fooAtomic;
}
}
// fooNonnull getter&setter
- (id)fooNonnull {
// here the developer need to ensure that _fooNonnull must not be nil
NSAssert(_fooNonnull, @"fooNonnull must not be nil!");
return _fooNonnull;
}
- (void)setFooNonnull:(id)fooNonnull {
NSAssert(fooNonnull, @"fooNonnull must not be nil!");
_fooNonnull = fooNonnull;
}
// fooCopy setter
- (void)setFooCopy:(id)fooCopy {
_fooCopy = [fooCopy copy];
}
// fooWeak setter
- (void)setFooWeak:(id)fooWeak {
_fooWeak = fooWeak;
}
// flag getter
- (BOOL)isFlag {
return _flag;
}
@end
参阅链接
- developer.apple.com/library/arc…
- www.jianshu.com/p/035977d1b…