写了一些 iOS 平台上的代码后,我发现自己对于 @property 这个 Objective-C 中很重要、最经常使用的特性不是很熟悉,而这也影响到了我的开发质量。特别是,@property 存在许多选项,nonatomic、weak、assign、copy 等等一系列配置,我弄了很久还是不能分辨清楚,看到开源项目中的用法也会搞不清楚原由,更别提合理地使用。所以我决定好好地研究一下这个问题。

@property 是 Objective-C 对封装的实现

我们常常听人说,面向对象的方法论有四个原则:封装、抽象、继承以及多态,这其中的“封装”的意思是,内部数据变更的实现细节不要暴露给客户代码,而是提供稳定一致的 API 来使其和自己交互——这样一来就可以在内部实现变更的情况下最大程度的保证客户代码不会出现版本兼容问题。也就是说,封装的意义是指,当一个对象内部定义持有数据成员,而客户代码需要和这些数据成员来进行交互时,原则上我们不能让外界直接和它交互,而应该提供我们可控的外界与其交互的接口方法。简而言之,我们用方法来封装数据,从而达到控制的目的

What is encapsulation? Well, in a nutshell, encapsulation is the
hiding of data implementation by restricting access to accessors and
mutators.

摘自 4 major principles of Object-Oriented Programming

Encapsulation is the packing of data and functions into a single component. The features of encapsulation are supported using classes in most object-oriented programming languages, although other alternatives also exist. It allows selective hiding of properties and methods in an object by building an impenetrable wall to protect the code from accidental corruption.

In programming languages, encapsulation is used to refer to one of two related but distinct notions, and sometimes to the combination thereof:

  • A language mechanism for restricting access to some of the object’s components.
  • A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data.

摘自 Wikipedia)

“我们可控的”的意思是,在这些控制数据成员获取、修改的方法中我们可以根据自己的需要加入其他控制相关的代码——例如提供原子操作的线程锁——这样一来我们就可以在客户代码无感知的情况下做更多的和数据成员相关的事情。下面举几个例子,可以更好地阐释何为封装。

Java 中经常被使用的 getter、setter 模式是一种封装的实现:

1
2
3
4
5
6
7
8
9
10
11
class Fridge {
private int cheese;

void setCheese(int _cheese) {
cheese = _cheese;
}

int getCheese() {
return cheese;
}
}

Python 中的 @property 装饰器是一种封装的实现(代码取自我正在开发中的 pyalgor 项目,TNode 实现了一个普适的树节点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class TNode(object):
def __init__(self, key, value, height,
parent, children=None):

self._key = key
self._value = value
self._parent = parent
self._height = height

if children is None:
self._children = []
else:
self._children = children

@property
def key(self):
return self._key

@key.setter
def key(self, key):
self._key = key

@property
def value(self):
return self._value

@value.setter
def value(self, value):
self._value = value

@property
def height(self):
return self._height

@height.setter
def height(self, height):
self._height = height

@property
def parent(self):
return self._parent

@parent.setter
def parent(self, parent):
self._parent = parent

@property
def children(self):
return self._children

@property
def children_len(self):
# 一个数据成员不一定是对象的成员变量,还可以是某个实时计算值
return len(self._children)

尽管 Python 中并没有真正意义的拥有 private、protected 级别 accessibility 的属性,但是把它作为一种 convention 来看待的话,@property 就是一种数据的封装机制。更巧的是,Python 中的 @property 和 Objective-C 中的 @property 的名字是一样的。

在面向对象的理论中,对数据属性进行封装的方法分为两种:一种是获取数据元素值的 Accessor,一种是修改数据元素值 Mutator。在上面的两个例子中,Java 中 get 开头的方法、Python 中 @property 装饰的方法是 Accessor,Java 中 set 开头的方法、Python 中 @属性名.setter 装饰的方法是 Mutator。

@property 就是 Objective-C 提供的封装机制的实现——它让我们可以方便地控制对象中的数据成员与客户代码交互的过程,并且通过一些配置简化掉了许多重复的工作。

Apple 的官方文档对 @property 的介绍是这样的:

Objects often have properties intended for public access.

Objective-C properties offer a way to define the information that a class is intended to encapsulate.

如何使用 @property

接下来谈谈如何使用 @property。我们先从最基本的操作来谈起,然后再谈谈配制 @property。

声明、赋值、获取

一个最简单的 @property 的使用的例子如下:

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property NSString *myString;

@end

通过 @property 这一行代码,我们就在 MyObject 中声明了一个新属性,或者说新的 property。一个 property 等于数据成员、Mutator、Accessor 的和。通过定制各个组成部分,property 可以表现出不同的行为特征。

在默认情况下,上面的代码会导致编译器在 MyObject 中添加 setMyString: 和 myString 方法,并且自动地在 MyObject 的实例中添加一个 NSString 类型的实例变量 _myString。_myString 就是数据成员,setMyString: 是 _myString 的 Mutator,myString 是 _myString 的 Accessor。

在 Apple 官方文档的 Encapsulating Data 一章中,这种类型的 property 被称之为 “instance variable backed property”,它的意思是说,这种类型的 property 的背后是有一个实例变量在保存它的值的。

一旦定义好了一个 property 后,我们就可以用如下方式来赋值、获取一个对象上的 property:

1
2
3
4
5
6
7
MyObject *object = [[MyObject alloc] init];

// 用 Mutator 方法给 property 赋值
[object setMyString:@"Hello Property"];

// 用 Accessor 获取 property 的值并打印
NSLog(@"%@", [object myString]);

在 Objective-C 演化到 2.0 版本后,dot-notation 被添加到了 @property 的语法当中,所以我们可以用下面的这种方式来赋值、获取了:

1
2
3
4
5
6
7
MyObject *object = [[MyObject alloc] init];

// 用“.”操作符给 property 赋值
object.myString = @"Hello Property";

// 直接用“.”操作符来获取 property 的值并打印
NSLog(@"%@", object.myString);

在 Apple 给出的文档中,对 dot-notation 的介绍是这样的:

Dot syntax is purely a convenient wrapper around accessor method calls. When you use dot syntax, the property is still accessed or changed using the getter and setter methods mentioned above:

  • Getting a value using somePerson.firstName is the same as using [somePerson firstName]
  • Setting a value using somePerson.firstName = @”Johnny” is the same as using [somePerson setFirstName:@”Johnny”]

This means that property access via dot syntax is also controlled by the property attributes. If a property is marked readonly, you’ll get a compiler error if you try to set it using dot syntax.

这意味着,dot-notation 本质上就和 Java 中 String 类支持“+”操作符一样,是一个语法糖。本质上你还是在使用 Accessor 和 Mutator 来和数据成员在做交互。

我在使用 dot-notation 的过程中,有时候会发现如果连续地使用“.”操作符来赋值的话,编译器会报错。后来我发现第一个“.”是 dot-notation,第二个点是 struct 的“.”操作符,最后因为编译器的转化,整个语句就会报错了——这一点确实很让人迷惑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property CGPoint myPoint;

@end

// MyObject.m

#import "MyObject.h"

@implementation MyObject

@end

// main.m

#import "MyObject.h"

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject *obj = [[MyObject alloc] init];

// 在这一行编译器会报错:“Expression is not assignable”
obj.myPoint.x = 1.0;
}
return 0;
}

要想不让上面的代码报错,而又达到我们的目的的话,main.m 中的代码就只能这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "MyObject.h"

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject *obj = [[MyObject alloc] init];

CGPoint objPoint = obj.myPoint;
objPoint.x = 2.33333;
obj.myPoint = objPoint;
}
return 0;
}

互联网上对于 dot-notation 的优缺点也存在着非常多的讨论()。

虽然 dot-notation 存在一些缺点,但是包括 NYTimes Objective-C Style Guide 在内,大部分规范都是推荐使用它来和数据成员进行交互的——这样一来代码在表意上会更清晰

定制 @property

方才的 @property 声明语句定义的 property 是拥有默认行为的 property。根据一定的特殊需求,我们可以对 @property 声明进行定制,进而控制 property 本身的行为——这些定制主要都发生在 Mutator、Accessor 上。一个例子如下:

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property (readonly) NSString *myString;

@end

上面的声明语句中,我们多加了一个 (readonly) 来讲 myString 定制为只读的属性。这样一来,编译器就只会给 myString 属性生成 Accessor,而不会生成 Mutator:

因此,当客户代码想要设置 myString 的值时,就会因为没有 setMyString: 方法而无法实现。要设置 myString 的值则可以在 MyObject 上定义一个修改 myString 的 backing instance variable,也就是 _myString,的方法然后将该方法暴露出去,这相当于我们自己手动实现了 Mutator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property (nonatomic, readonly) NSString *myString;

- (void)setMyString:(NSString *)myString;

@end

// MyObject.m

#import "MyObject.h"

@implementation MyObject

- (void)setMyString:(NSString *)myString {
_myString = myString;
}

@end

当然这样做的话,是违背了 readonly 的设计初衷的,举这个例子是为了辅助理解 @property,请不要这样做。

###@property 语句还有其他选项

类似于 readonly 这样的定制 @property 声明的选项一共有八个,它们形成了四对相反的选项,分别是 nonatomic 与 atomic、readwrite 与 readonly、copy 与 assign、strong(retain)与 weak 等。刚开始写 Objective-C 代码的时候,因为对于这些选项并不是很清楚,我犯了挺多的错误。下面我们来进一步地一次介绍这些选项的作用。

  1. nonatomic 与 atomic。这两个属性控制了数据成员的交互的原子性()。这里所谓的原子性是指,当数据成员处在多线程的环境之下时,在每一个时间点有且只有一个线程在和数据成员进行交互,它和线程安全的概念并不是等同的,这是因为线程安全的要求更高——它不仅仅要求单个数据成员拥有交互的原子性,还进一步地要求各个数据成员组合在一起并暴露在多线程的环境下时在任何时间点不会出错。原子性的操作是不可再分的,所以进一步是线程互斥的。如果声明为 nonatomic,则该 property 的数据成员的交互不是原子性的,在同一时间允许多个线程对数据成员进行赋值、获取。如果声明为 atomic 的话,则该 property 的数据成员的交互时原子性的,在同一时间只允许同一个线程来执行赋值、获取。声明为 atomic 的话,有时会导致多线程访问数据成员的停等,效率不高,所以在实际工程项目中,如果特殊要求,一般都会将能声明为 nonatomic 的 property 声明为 nonatomic。我并没有深入地探索编译器是如何实现这个配置的,但我猜测它应该是在数据成员的 Accessor、Mutator 方法中加入了线程锁。
  2. readwrite 与 readonly。这两个属性决定了数据成员可访问性——是只读的还是可读可写的(很奇怪的是并没有 writeonly 这个选项,你只能这样实现)。声明为 readwrite 的话,数据成员是可读可写的,这个时候编译器会同时生成 Accessor 和 Mutator。声明为 readonly 的话数据成员就是只读的,这个时候编译器只会生成 Accessor。
  3. copy 与 assign。这两个属性控制了你通过 Mutator 获取数据成员值的时候是获得的其值的复制还是数据成员的原始指针——这两种情况的区别是,当你获得的是数据成员值的 copy 时,你修改这份 copy 的值后并不会影响数据成员的原值,而如果是原始指针的话则有可能会影响原值,这取决于数据成员是 object 还是原始类型。需要注意的是,copy 只能用在 object 类型的数据成员上,并且该数据成员的类型必须实现了 NSCopying Protocol——这是因为,对于原始类型,当你声明另一个变量名来保存另一个变量保有的值时,默认的就是按值传递,新声明的变量和被保存的变量并不是同一块内存,也就不存在按值传递还是按引用的问题。对于 object 类型的数据成员,则存在是直接返回原指针给外界还是复制一份后将复制后的新对象的指针给外界的两种选项。总而言之,原始类型的数据成员只有 assign 一个选项,object 类型的存在 copy、assign 两个选项。
  4. strong(retain)与 weak。这一对选项决定了保有数据成员的指针是强指针还是弱指针。strong 和 retain 的语义等同,是 ARC 出现后 retain 的替代,retain 被用在非 ARC 环境下,在非 ARC 环境下 strong 只是 retain 的另一种写法,现在的代码一律写 strong 即可。保有数据成员的指针是强指针还是弱指针直接影响到 ARC 中的内存管理机制——只要一个对象还被 strong 指针所保有,系统便不会对其进行回收;但若是一个对象被 weak 指针所保有,系统便会对其进行销毁、回收,而该 weak 指针也会被重置为 nil 来防止野指针问题的发生。在一些情况中,对象之间会出现相互引用的情况,这个时候若两个对象保有的都是相互的 strong 指针,那这两个对象之间就形成了 strong 指针循环,系统便无论如何都无法回收这两个对象。顺便一提,默认的情况下,局部声明的指针都是 strong 指针。

默认的 @property 配置是这样的:atomic、readwrite、strong(retain)、assign,这一点请务必记住。若你声明的 property 本身就已经是这个类型的了,就不必要再写出来了。

这些配置老实说确实是会让初学者比较头疼的,需要时间和实验来帮助我们消化。Apple 的官方文档有更详细的解释,大家可以看看。

@synthesize 和 @dynamic

还有两个和 @property 紧密相关的语句——@synthesize 和 @dynamic。

先说说 @synthesize 语句。

@synthesize 语句只能被用在 @implementation 代码段中,通过这句语句可以完成某个由 backing instance variable 保存数值的 property 的 backing instance variable 的名字的定制,而默认的 backing instance variable 的名字就是 property 名字前加一个“_”符号。这句话可能有点绕,我们举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property NSString *myString;

@end

// MyObject.m

#import "MyObject.h"

@implementation MyObject

@end

上面的代码对于 myString 这个 property 使用了默认的 backing instance variable 名字 _myString:

但当我们的代码如下时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property NSString *myString;

@end

// MyObject.m

#import "MyObject.h"

@implementation MyObject

@synthesize myString = holyString;

@end

myString 这个 property 的 backing instance variable 的名字就是 holyString 了:

如果将上面的“@synthesize myString = holyString;”改为“@synthesize myString;”的话,backing instance variable 的名字就是 myString 了。

再来简要地说说 @dynamic 语句。

正如其名,@dynamic 声明了一个“动态”的 property,这个 property 的 Accessor 和 Mutator 将会在别处(父类、子类等地)定义或者在运行时中动态定义。

Stack Overflow 上的这个解释比较不错:

Some accessors are created dynamically at runtime, such as certain ones used in CoreData’s NSManagedObject class. If you want to declare and use properties for these cases, but want to avoid warnings about methods missing at compile time, you can use the @dynamic directive instead of @synthesize.

Using the @dynamic directive essentially tells the compiler “don’t worry about it, a method is on the way.”

Apple 的官方文档也有较为详细的解释。

使用 @property 时需要注意的问题

老实说,由于之前大量用过 Python 这门设计上相对而言更为精良的语言,当我开始使用 Objective-C 后,我一时半会儿有些难以适应。特别是,正如 Java 一样,Objective-C 中存在着大量程序员不友好的设计缺陷,这会让初学者面临各种踩坑的风险。又正如 Java 的思路一样,“语言有缺陷,模式来弥补”,当面对语言的缺陷时,程序员们被要求记住各种“闪避用”的使用模式(姑且认为它们还没有进化到设计模式的级别吧)。

接下来我们就来谈谈和 @property 相关的一些需要注意的问题。

在 init 方法中一定要直接访问数据成员

在一个对象的 init 方法中,你一定只能这样初始化各个 property:

1
2
3
4
5
6
7
8
9
- (instancetype)init {
self = [super init];

if (self) {
_myString = @"Futrurama!";
}

return self;
}

而不能这样:

1
2
3
4
5
6
7
8
9
10
- (instancetype)init {
self = [super init];

if (self) {
self.myString = @"Futurama!";
// 或者 [self setMyString: @"Futurama!"];
}

return self;
}

为什么需要这样处理呢?Apple 在官方文档中是这样解释的:

Setter methods can have additional side-effects. They may trigger KVC notifications, or perform further tasks if you write your own custom methods.

You should always access the instance variables directly from within an initialization method because at the time a property is set, the rest of the object may not yet be completely initialized. Even if you don’t provide custom accessor methods or know of any side effects from within your own class, a future subclass may very well override the behavior.

也就是说,当你通过 Mutator 来设置一个 property 的值时,由于 @property 声明 property 会对该 property 自动实现 KVO 的机制,和该 property 相关联的一些 Observer 的代码可能会被触发——而又由于此时是 init 阶段,可能 Observer 的代码的一些相关依赖条件还没有满足,进而就可能导致不确定性行为。

###对于 copy 类型的 property,赋值时要用对象的 copy

在 init 阶段,你一只能这样初始化一个带有 copy 声明的 property:

1
2
3
- (id)initWithSomeOriginalString:(NSString *)aString {
    self = [super init];
if (self) { _instanceVariableForCopyProperty = [aString copy]; }
return self; }

而不能这样:

1
2
3
- (id)initWithSomeOriginalString:(NSString *)aString {
    self = [super init];
if (self) { _instanceVariableForCopyProperty = aString; }
return self; }

为什么需要这样呢?Apple 官方的文档并没有详细解释。我的理解是,当出现下面情况时会出现问题:

1
2
3
4
5
6
NSMutableString *myString = @"Hello";

MyObject *object = [[MyObject alloc] initWithSomeOriginalString:myString];

// 初始化后马上修改 myString,这样 object 保有的值也被改变了,违背了上一行代码只保有 @"Hello" 的意图
[myString appendString:@" World!"];

在 Accessor 和 Mutator 也一定要直接访问数据成员

在 Accessor 和 Mutator 中,一定要这样:

1
2
3
4
5
6
7
- (void)setMyString:(NSString *)myString {
_myString = myString;
}

- (NSString *)myString {
return _myString;
}

而不能这样:

1
2
3
4
5
6
7
8
9
- (void)setMyString:(NSString *)myString {
self.myString = myString;
// 或者 [self setMyString:myString];
}

- (NSString *)myString {
return self.myString;
// 或者 return [self myString];
}

这是因为,如果按照后面那种方法来实现 Accessor、Mutator,将会导致 Accessor、Mutator 自己递归地调用自己,最终导致 Stack Overflow:

property 的背后不一定存在 backing instance variable

在 Apple 的官方文档中对于这个问题是这样描述的:

The compiler will automatically synthesize an instance variable in all situations where it’s also synthesizing at least one accessor method. If you implement both a getter and a setter for a readwrite property, or a getter for a readonly property, the compiler will assume that you are taking control over the property implementation and won’t synthesize an instance variable automatically.

当你 override 了 readonly 的 property 的 Accessor,或者 readwrite 的 property 的 Accessor 和 Mutator 后,编译器就不会自动地给这些 property 生成 backing instance variable 了:

解决这个问题的方法是明确地用 @synthesize 来生成 backing instance variable。

还有一个很典型的例子发生在利用 Category 机制给已经存在的类添加 property 时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface UIViewController (MyCategory)

@property (nonatomic) NSString *myString;

@end

static const void *myStringKey = &myStringKey;

@implementation UIViewController (BATabBarController)

- (void)setMyString:(NSString *)myString {
objc_setAssociatedObject(self, myStringKey, myString, OBJC_ASSOCIATION_ASSIGN);
}

- (NSString *)myString {
return objc_getAssociatedObject(self, myStringKey);
}

上面的代码利用 Objective-C Runtime 的 Associated Object 机制给 UIViewController 添加了我们自己定制的 myString property,并没有用到 backing instance variable。

对象的 delegate 不要使用 strong 指针

为了避免 delegate 对象和 delegate 拥有者对象之间的 strong 指针循环,一般最好将 delegate property 声明为 weak 指针保有的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <Foundation/Foundation.h>

@class MyObject;

@protocol MyObjectDelegate <NSObject>

@optional

- (void)someDelegateMethod:(MyObject *)object;

@end

@interface MyObject : NSObject

@property NSString *myString;

@property (nonatomic, weak) id<MyObjectDelegate> delegate;

@end

这一点在 Apple 官方文档的 Avoid Strong Reference Cycles 这一章有详细解释,在这里就不赘述了。

weak 指针保有的 @property 引起的一些问题

在前文我们曾经提到过,对于由 weak 指针保有的对象,当没有其他 strong 指针再保有它时,系统会在必要时将该对象回收,并将 weak 指针重置为 nil。这样的机制给类似于下列形式的代码带来了一个问题:

1
2
3
4
- (void)someMethod {
[self.weakProperty doSomething];
[self.weakProperty doSomethingElse];
}

如果在 someMethod 被调用时,self.weakProperty 被系统回收后重置为 nil,那接下来的一系列操作可能都不能正常进行,所以我们需要这样处理:

1
2
3
4
5
- (void)someMethod {
NSObject *strongPointer = self.weakProperty;
[strongPointer doSomething];
[strongPointer doSomethingElse];
}

前文已经提到过,局部变量指针默认为 strong 指针。所以,通过上述代码的处理,self.weakProperty 保有的对象就拥有 strongPointer 这个 strong 指针保有,接下来的调用行为就会正常进行了。

下面的代码也会有类似的问题,你觉得该如何处理呢?

1
2
3
4
5
- (void)someMethod {
if (self.weakProperty) {
...
}
}

@synthesize 的作用到底是什么

从互联网上的这篇 2010 年的帖子来看,@synthesize 的主要功能是生成 Accessor 和 Mutator,并且指定它们背后的 backing instance variable 的名字。所以我们可以合理地推测像下面的写法就和一个有 backing instance variable 的 property 是等价的了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject {
NSString *myString;
}

- (void)setMyString:(NSString *)myString;

- (NSString *)myString;

@end

// MyObject.m

#import "MyObject.h"

@implementation MyObject

@synthesize myString = myString;

@end

然而这样写却会报编译错误:

/Path/To/MyObject.m:13:13: Property implementation must have its declaration in interface ‘MyObject’

对于这个问题,在线上给出的解答()都是说要添加 @property 声明语句就不会出问题了,但是却并没有具体解释原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property NSString *myString;

@end

// MyObject.m

#import "MyObject.h"

@implementation MyObject

@synthesize myString = myString;

@end

可是如果在方才代码的基础上不用 @synthesize 语句的话,代码也是正确的,而且 Accessor 和 Mutator 以及默认的 backing instance variable 都会自动生成,就好像 @synthesize 自动被使用了一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property NSString *myString;

@end

// MyObject.m

#import "MyObject.h"

@implementation MyObject

@end

老实说这些代码行为真的整的人比较头大——太不符合人的直觉了。更为无奈的是,Objective-C 并没有一份权威的 Language Standard/Specification 或者 Language Reference 来解释各个关键语句、关键字确切的行为(Java 都做的比它好,那就更别提 Python 是怎么做的了),只有一份不够详细、不够准确的 Programming with Objective-CApple 实现的 compiler 这个黑盒子成为了这门语言事实上的标准(clang 的线上文档中倒是有一些和 Objective-C 相关的,但是我暂时还没找到 @synthesize 相关的文档),实在是比较遗憾。

吐槽完毕,我们来解释一下以上代码行为的原因:自 Xcode 4.4 版本所带的编译器 clang 3.2 开始,如果你不为一个 @property 语句写 @synthesize,编译器会自动为该 property 执行 “@synthesize propertyName = _propertyName”,这种编译器行为被称为“default synthesis”:

As of clang 3.2 (circa February 2012), “default synthesis” (or “auto property synthesis”) of Objective-C properties is provided by default. It’s essentially as described in the blog post you originally read: http://www.mcubedsw.com/blog/index.php/site/comments/new_objective-c_features/ (except that that post describes the feature as “enabled, then disabled”; I don’t know if that’s an issue with Xcode or if the clang developers themselves have gone back and forth on the question).

As far as I know, the only case in which properties will not be default-synthesized in clang 3.2 is when those properties have been inherited from a protocol.

摘选自救世主 Stack Overflow

讨论至此,我觉得 @synthesize 的作用就只有之前提到的定制 backing instance variable 名字的作用了

小结

到这里基本上和 @property 相关的知识我们就总结的差不多了。想要更详细地了解可以去参考 Apple 的 Programming with Objective-C。如果文档里面有任何问题、错误的话,欢迎大家在评论里面指出。

最后用我最爱的 Bender) 结尾: