携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 2 天,点击查看活动详情
1. 背景
本文假定读者已经了解了 OC 的基础语法, 这包括类/变量的定义, 对象的实例化与方法的声明与调用等.
1.1 定义样例类 - People
@interface People : NSObject {
@public
float weight;
}
- (void)eat:(float)food_weight;
@end
@implementation People
- (void)eat:(float)food_weight {
weight += food_weight;
}
@end
1.2 变量的封装
先来看一个调用 People 类的例子:
int main() {
@autoreleasepool {
People *people = [[People alloc] init];
people -> weight = 1;
[people eat: -2];
NSLog(@"Weight is %f kg after eat.", [people weight]);
}
return 0;
}
这个例子中, 我们通过 eat 方法让对象的 weight 属性修改为了负数, 又直接修改了 weight. 这在程序运行的角度上不会出现问题, 但在现实生活或业务场景中, 我们往往希望限制对数据的访问行为, 这就需要用到封装思想.
对数据封装的典型方法是将数据声明为 @private, 在通过设定的接口供外部访问数据, 这种接口就被称为 getter (查询者) 和 setter (修改者).
定义了 get 和 set 的成员变量可以是用点语法 (pointer.member) 和指针语法 (pointer->member) 访问, 点后的是访问 getter 的名称.
@interface People : NSObject {
float _weight;
}
- (void)eat:(float)weight;
- (void)setWeight:(float)weight;
- (float)weight;
@end
@implementation People
- (void)eat:(float)weight {
self.weight += weight;
}
- (void)setWeight:(float)weight {
if (weight < 0) { NSLog(@"Illegal weight setting!"); }
else { _weight = weight; }
}
- (float)weight { return _weight; }
@end
int main() {
@autoreleasepool {
People *people = [[People alloc] init];
people.weight = 1;
[people eat: -2];
NSLog(@"Weight is %f kg after eat.", [people weight]);
}
return 0;
}
这段程序的运行结果是:
> Illegal weight setting!
> Weight is 1.000000 kg after eat.
对变量进行封装是很好的习惯, 但是当变量的数目不断增多时, 手动添加 set 和 get 就显得繁琐而不划算. 为此 OC 提供了属性 (property) 来代替 set 和 get 实现数据的封装.
2. 属性 - @property
property 的语法:
@property ([trait 1], [tarit 2], ...) [type] [name];
// Sample
@property float weight;
// ... which equals to
{ @private float _weight; }
- (void)setWeight:(float)weight;
- (float)weight;
2.1 属性的特性 (trait)
2.1.1 原子性
默认情况下属性是 atomic 的, 生成的 set 和 get 都具有线程安全特性. 可以为属性添加 nonatomic 特性, 这样可以关闭属性的原子性.
具有原子性的属性无法被其他线程获取修改时的中间状态, 这意味着对于外部来说, 其只有变动前和变动后, 不存在正在变动的情形.
2.1.2 读写权限
readwrite 和 readonly 可以控制使用 @synthesize (后文属性的实现部分会提到) 后编译器是否自动生成 set 方法. 这两个特性是互斥的. 一个特性默认是 readwrite 可读可写的.
2.1.3 访问/存储方法名
假使属性名为 name, 则默认的 get 方法名为 name, set 方法名为 setName. 不过仍可以通过 getter 和 setter 特性改变方法名.
@property (getter = isOn) BOOL on;
Boolean 类型通常都将自己的 getter 命名为
isXxx.
2.1.4 set 语义
set 方法有多种不同的实现方式, 可以通过特性在不同的语义之间切换.
assign: 默认值 简单的让旧值等于新值, 没有什么额外的操作strong: 此特性标示所属关系, 为这种属性设置新值时, set 方法会增加新值的引用计数后减少旧值的引用计数, 最后一个 strong 引用释放后对象销毁, 在 ARC 开启时使用.weak: 仅持有引用, 设置新值时也不持有新值的所有权, 更不会释放旧值占用的资源. 与assign属性不同的是, 如果持有值被销毁了, 属性值会被自动设置为 nil, 好处不言而喻, 在 ARC 开启时使用.copy: 会在赋值时将目标对象复制一份, 属于内容拷贝, 赋值对象必须实现NSCopying协议.retain: 会复制目标对象的地址, 属于地址拷贝, 会给目标对象的引用计数加 1, 同时旧对象的引用计数减 1, 行为和 strong 非常相似.
ARC 是 Automatic Reference Counting,
strong与retain属于不同时代的产物, 因此行为虽然相似但还是分成了两个关键字. 现在一般都是开启 ARC 的.
set 还有一些其他语义, 但整体上都是围绕复制造成的地址申请和释放来构造的, 如果理解智能指针和深浅拷贝以及其他的复制时问题, 理解起来会更容易.
属性的实现
@synthesize 关键字会自动实现协议中定义的属性.
@synthesize [name] = _[name]
// Sample
@synthesize weight = _weight
// ... which equals to (default)
- (void)setWeight:(float)weight { _weight = weight; }
- (float)weight { return _weight; }
该关键字会通知编译器自动实现 @implementation 中没有实现的 set 和 get 方法.
较旧版本
@synthesize和@property是成对出现的, 也就是说要手动使用@synthesize来合成相应的存取方法, 否则不会自动合成 (现在编译器默认会自动添加@synthesize自动合成存取方法).
3. 实践
下面是一个基础的例子, 在 main.m 中执行之后会打印出 “Hello, Alice.” 的字样.
#import <Foundation/Foundation.h>
@interface People : NSObject
@property NSString* name;
@end
@implementation People
// @synthesize name = _name; // ..which is optional in current version.
@end
int main() {
@autoreleasepool {
People *people = [[People alloc] init];
people.name = @"Alice";
NSLog(@"Hello, %@.", people.name);
}
return 0;
}
进一步尝试所有的 property trait: ObjectiveC @Property practice - Github Gist
参考文献
- 传智播客高教产品研发部. Objective-C 入门教程[M]. 2015年版. 人民邮电出版社, 2015.
- 半纸渊. Objective-c 知识总结 -- @property[EB/OL]. [2022-7-29]. www.jianshu.com/p/59b218a88….