原文地址:http://www.iphonedevsdk.com/forum/iphone-sdk-tutorials/7295-getters-setters-properties-newbie.html
假设有个MyClass类,里面定义了一个成员text。对它的读与写如下所示:
//MyClass.h file
@interface MyClass: NSObject {
NSString *text;
}
-(void) init;
-(void) logText;
@end
//MyClass.m file
@implementation MyClass
- (void)init {
text = @"some text";
}
- (void)logText {
NSLog(@"%@", text);
}
@end
上面的程序是用常量初始化text,所以不用关心内存管理。
不过,我们肯定是希望在运行时能够改变它,那么需要添加新函数:
//MyClass.h file
@interface MyClass: NSObject {
...
-(NSString*) text; // 这就是getter的声明
-(void) setText:(NSString *)textValue; // 这就是setter的声明
@end
//MyClass.m file
@implementation MyClass
...
// 这就是getter的定义
-(NSString*) text {
return text;
}
// 这就是setter的定义
-(void) setText:(NSString *)textValue {
if (textValue != text) // 若两个对象不同
{
[textValue retain]; // “传入对象”的引用计数加一
[text release]; // text的引用计数减一(释放堆内存)
text = textValue; // 给text赋新值
}
}
-(void)dealloc {
[text release]; // 对象的析构函数里要释放text
[super dealloc];
}
@end
于是,就可以像下面那样读、写text的值:
NSString *theTextValue = [obj text]; // 读
[obj setText:newStringValue]; // 写
因为iOS对NSObject对象的管理是采用引用计数的模式,所以setter要写成上面的样子。当然,也可以写成下面这样:
-(void) setText:(NSString *)textValue {
[textValue retain];
[text release];
text = textValue;
}
去掉了if语句,功能也是正常的,顶多就是对同一对象的引用计数“加一又减一”而已。
但是不能写成下面的样子:
-(void) setText:(NSString *)textValue {
[text release]; // 先减一
[textValue retain]; // 再加一
text = textValue;
}
这是不行的。先减一的话,textValue可能会因为引用计数为零而被释放。对已死的对象再“加一”就不行了。
而且这种bug是隐性的,不会每次都发作。需要程序员用“好习惯”来纠正。
Objective C有一个非常棒的特性:给nil对象发消息是安全的。这就像在C++里回收一个空指针的资源一样安全。
下面给MyClass添加另外一个成员,还有它的getter和setter。
//MyClass.h file
@interface MyClass: NSObject平共处{
NSString *text;
int value;
}
-(void) init;
-(void) logText;
-(NSString*) text;
-(void) setText:(NSString *)textValue;
-(int) value;
-(void) setValue:(int*)intValue;
@end
//MyClass.m file
@implementation MyClass
- (void)init {
text = @"some text";
value = 2;
}
- (void)logText {
NSLog(@"%@", text);
}
-(NSString *) text {
return text;
}
-(void) setText:(NSString *)textValue {
if (textValue != text)
{
[textValue retain];
[text release];
text = textValue;
}
}
-(int) value {
return value;
}
-(void) setValue:(int)intValue {
value = intValue;
}
-(void)dealloc {
[text release];
[super dealloc];
}
@end
这次添加的是int型的成员,所以无需retain、release。
可以看到,getter和setter是如此的常见,所以Objective C提供了一个捷径:
//MyClass.h file
@interface MyClass: NSObject {
NSString *text;
int value;
}
@property(nonatomic, retain) NSString *text;
@property(nonatomic, assign) int value;
-(void) init;
-(void) logText;
@end
//MyClass.m file
@implementation MyClass
@synthesize text;
@synthesize value;
- (void)init {
text = @"some text";
value = 2;
}
- (void)logText {
NSLog(@"%@", text);
}
-(void)dealloc {
[text release];
[super dealloc];
}
@end
通过property声明和synthesize定义,Objective C自动生成了getter和setter。而且功能和上面的代码完全一样。
下面讲解一下property里声明的属性。
@property(nonatomic, retain) NSString *text 意为:我有个成员叫text,无需多线程保护,在setter里要使用retain/release过程。
@property(nonatomic, assign) int value 意为:我有个成员叫value,无需多线程保护,在setter里直接赋值就可以,不需要引用计数机制介入。
除了“方框引用法”,Objective C还提供了“点引用法”,来引用成员、成员函数。
NSString *s = [obj text];
[obj setText:@"new string"];
int i = [obj value];
[obj setValue:3];
用点引用法,就可以写成:
NSString *s = obj.text;
obj.text = @"new string";
int i = obj.value;
obj.value = 3;
注意,二者在功能上是完全一样的,都是对getter和setter的调用!用点引用法只是看起来简洁一点而已。
对于Java程序员来说,下面的写法很常见:
void doSomething(String text) {
this.text = text;
}
如果把Objective C的程序写成上面的样子:
-(void)doSomething:(NSString *)text {
self.text = text;
}
意义就完全不一样了!它不是对成员的直接引用,而是对setter的直接引用!换言之,是对(void)setText:(NSString *)text的调用。
这有着非常大的不同!其一,setter会有引用计数;其二,调用setter时,有可能该成员还没被初始化。
再看下面的函数,一样具有迷惑性:
-(void)doSomething:(NSString *)input {
text = input; // 直接对成员赋值。对NSString对象来说,会有内存泄漏
self.text = input; // 会调用setter。保证了成员先释放再赋值,安全
}
所以,使用“self.text =”这种赋值形式是好习惯。
这就解释了,为什么你会在iOS的程序里看到很多下面这样的代码:
-(void)doSomething {
SomeObject *obj = [[SomeObject alloc] init];
self.obj = obj;
[obj release];
}
第一行:分配、初始化后,obj就有了“一个”引用。
第二行:用setter增加引用计数,obj现在有了“两个”引用。
第三行:obj使用结束,引用计数减一,现在是“一个”引用。
所有这些,都可以通过在Xcode的GDB Debugger里用“p [obj retainCount]”来验证。
所以,内存管理的基本原则有两条:
1. 如果你alloc、create、copy了一个对象,最后你要release它;
2. 如果你retain一个对象,最后你要release它。
对象的直接赋值只能出现在一个地方:init。因为它只在alloc之后调用一次:
obj = [[SomeObject alloc] init];
而且,此时所有成员都是nil,直接赋值无妨。
如果在init里不直接赋值会怎么样?会出问题!
self.obj = [[SomeObject alloc] init];
像这样使用了setter的,obj会有两个引用!一个对象刚刚初始化就有了两个引用!它很可能永远不会被释放了。内存泄漏啊。
如果想使用“点引用法”,同时又对编译器产生的setter不满意,怎么办?
以上面的程序为例,如果我们想把text和value联系起来(在修改value的时候,text也跟着变),那么:
-(void) setValue:(int)intValue {
value = intValue;
self.text = [NSString stringWithFormat:@"%d", intValue];
}
同时,去掉@synthesize value,换成@dynamic value。这意味着你要提供自己的setter给编译器。
有时,你要创建一个对象给别人使用,那么你就无法选择时机去释放它了。怎么办?
- (MyObject *)getObject {
return [[MyObject alloc] init];
}
这样是不行的。使用者可能会在使用完后忘记释放它。根据程序界的规律,如果程序员可能忘,那他一定会忘。
解决办法是自动释放:
- (MyObject *)getObject {
return [[[MyObject alloc] init] autorelease];
}
调用了autorelease方法,系统会在NSAutoreleasePool里注册该对象。这样,“减引用计数”这个责任就落到了“自动释放池”的身上。池负责在合适的时机调用该对象的release方法。
再看上面提到的两条原则:
1. 对象的创建者有责任去释放。反之,不是你创建的,你也不要去释放它。使用者无责任。池会帮你释放。
2. 使用了retain的,仍然有责任去release。因为retain之后,增加了引用。池最后释放的时候,会因为该对象引用数不为零而不释放它。释放的责任在于使用者。
比如前面用到的代码:
-(void) setValue:(int)intValue {
value = intValue;
self.text = [NSString stringWithFormat:@"%d", intValue];
}
临时对象NSString的释放就是池负责的。如果该函数的上下文没有池,内存就泄漏了。
PS:原文说每个函数有个隐藏的池,所以退出函数时,这些对象会自动释放。我试验的结果是“绝对不会”。
我想用“点引用法”,又想让对象只读。即不生成setter。怎么办?
@property (nonatomic, readonly) PropertyType propertyName;
若想让对象可读可写,可以用:
@property (nonatomic, readwrite) ...
但这是编译器的默认行为,不写也可以。
编译器还默认对象为“atomic”,即多线程环境下保证安全。不会在多线程同时写,或者一个写一个读的时候发生数据破坏。
除非在你的程序中使用了多线程,否则所有的调用都在一个线程里进行。这样,加上nonatomic可以提高程序的运行效率。
给对象起“别名”也很有用。
@interface MyClass: NSObject {
NSString *_text;
}
@property(nonatomic, retain) NSString *text;
...
@synthesize text = _text;
这样,类的属性就更名为“text”了。
若想给getter起一个自己喜欢的名字,那么:
@property (nonatomic, assign, getter=isValue) boolean value;
除了retain和assign,还有一个copy操作:
copy只适用于NSObject类,而且实现了NSCopying协议。不能用于int这些基础数据类型。