Objective-C 总结与实践 - @property

213 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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 读写权限

readwritereadonly 可以控制使用 @synthesize (后文属性的实现部分会提到) 后编译器是否自动生成 set 方法. 这两个特性是互斥的. 一个特性默认是 readwrite 可读可写的.

2.1.3 访问/存储方法名

假使属性名为 name, 则默认的 get 方法名为 name, set 方法名为 setName. 不过仍可以通过 gettersetter 特性改变方法名.

@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, strongretain 属于不同时代的产物, 因此行为虽然相似但还是分成了两个关键字. 现在一般都是开启 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

参考文献

  1. 传智播客高教产品研发部. Objective-C 入门教程[M]. 2015年版. 人民邮电出版社, 2015.
  2. 半纸渊. Objective-c 知识总结 -- @property[EB/OL]. [2022-7-29]. www.jianshu.com/p/59b218a88….