(译)在Objective-c里面使用property教程
免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文链接地址:http://www.raywenderlich.com/2712/using-properties-in-objective-c-tutorial
教程截图:
这是在iphone上面使用objc,与内存管理有关的第三篇教程。
在第一篇教程中,我们介绍了在objective-c里面如果使用实例变量和引用计数来管理内存。
在第二篇教程中,我们介绍了如何检测内存泄露、与内存有关的易犯的错误,使用是Instruments以及其它辅助工具。
在这第三篇教程中,也是本系列的最后一篇教程,我们将谈一谈objc的property。我们将介绍property是什么,它是怎样工作的,有一些什么样的规则,以及使用它们可以用来避免大部分与内存相关的问题。
如果你还没有本系列教程的样例工程的话,可以点击这里下载,我们将从这个工程开始。
Retain Your Memory
让我们先回顾一下,本项目需要管理内存的地方在哪。
目前RootViewController有两个实例变量:_sushiTypes, 和 _lastSushiSelected.
@interface RootViewController : UITableViewController { NSArray * _sushiTypes; NSString * _lastSushiSelected; } @end
对于_sushiTypes,我们是在viewDidLoad里面通过alloc/init的方式来创建的,然后在viewDidUnload和dealloc里面release。
// In viewDidLoad. Afterwards, retain count is 1. _sushiTypes = [[NSArray alloc] initWithObjects:@"California Roll", @"Tuna Roll", @"Salmon Roll", @"Unagi Roll", @"Philadelphia Roll", @"Rainbow Roll", @"Vegetable Roll", @"Spider Roll", @"Shrimp Tempura Roll", @"Cucumber Roll", @"Yellowtail Roll", @"Spicy Tuna Roll", @"Avocado Roll", @"Scallop Roll", nil]; // In viewDidUnload and dealloc. Afterwards, retain count is 0. [_sushiTypes release]; _sushiTypes = nil;
对于_lastSushiSelected,它是在用户选中table view的一行时被赋值的。它在两个地方有release。一个是在赋值之前,还有一个是在dealloc时面,请看下面代码:
[_lastSushiSelected release]; _lastSushiSelected = [sushiString retain]; // In dealloc [_sushiTypes release]; _sushiTypes = nil;
这种方法肯定是可行的,但是,它需要你很认真的思考,每一次你给一个变量赋值的时候,都要认真考虑与之相关的内存问题。要不要先release后再赋值啊,要不要retain啊,总之,当变量一多,项目一大起来,各种内存问题就随之而来了。
因此,接下来,我会向你介绍一种简单的方法---使用property来管理内存。
搬把椅子过来,开始编码吧
如果你熟悉其它编程语言,比如java或者c#,于对getters和setters的概念肯定不陌生。当你拥有一个_sushiTypes的实例变量的时候,你经常需要让其它类的对象来访问这个变量。但是,如果直接使用.号的方式去访问不太好,它破坏了封装性的原则,把类的实现爆露给外面的,编程大师说的。不管你信不信,反正我是信了。:)
因此,你需要一个方法,叫做 “getSushiTypes”(或者仅仅是 “sushiTypes” ,这样少打了3个字母),同时,还需要一个方法,叫做“setSushiTypes”.通过使用这两个方法来访问类的实例变量。这是一个好的编码习惯,因为你可以改变实例变量的名字,但是你不会影响到其它类,因为接口没变。所以,我们编码代码的时候,也要多针对接口编码,少针对实现编码。当然,使用getter和setter还有其它好处,你可以在里面用NSLog输出一些内容,这样你就知道有没有人想窥探你的私有变量啦。相当于一个保镖。
像上面我所说的那样,为每个的的实例变量定义相应的getter和setter方法,当然,前提是你想让外部访问这个变量你才定义,你别搞得把全部变量都公开,那样封装的意义在哪里呢?这样,将会使内存管理的工作变得轻松。接下来,让我们看看,我是如何给这两个变量添加getters和setters的。
首先,在RootViewController.h里面,声明下面四个方法:
- (NSArray *)sushiTypes; - (void)setSushiTypes:(NSArray *)sushiTypes; - (NSString *)lastSushiSelected; - (void)setLastSushiSelected:(NSString *)lastSushiSelected;
然后,在RootViewController.m底部添加其实现:
- (NSArray *)sushiTypes { return _sushiTypes; } - (void)setSushiTypes:(NSArray *)sushiTypes { [sushiTypes retain]; [_sushiTypes release]; _sushiTypes = sushiTypes; } - (NSString *)lastSushiSelected { return _lastSushiSelected; } - (void)setLastSushiSelected:(NSString *)lastSushiSelected { [lastSushiSelected retain]; [_lastSushiSelected release]; _lastSushiSelected = lastSushiSelected; }
这里的getter方法很简单,它们只是返回各自的变量而已。
而setter方法,首先把传入的参数引用计数加1,同时把之前的实例变量引用计数减1,然后再把输入的变量赋值给实例变量。(译者:这里的写法其实不好,没有考虑自赋值的情况。如果大家也过C++的String类,那么写拷贝构造函数和赋值操作符的时候,是一定要考虑自赋值的情况的,不然会出问题。但是,上面作者的写法不会有问题。因为它先retain的,后release的。如果你写反了,先release,那么就出问题了。但是,如果我考虑自赋值的情况,那么我就不用考虑这种先后顺序的问题了。具体写法请参照我的原创,objc @property详解)。通过这种方式,新传入的参数被实例变量所引用,因为是所有者,所以符合“谁拥有,谁retain”的原则。
你可能会奇怪,为什么setter方法要先调用retain/release,然后再赋值,而且顺序不能变。当然啦,肯定是防止自赋值的情况啦。如果你还是搞不懂,那就算了吧,有些事情,总有一天你会明白的。:)
注意,这里为什么要把实例变量的命名前面加一个下划线呢?这样做一是可以使得getter和setter方法的参数命名获得方便。如果我们的实例变量命名为 “sushiTypes”,那么我们的setSushiTypes函数的参数名就不能再是“sushiTypes”,因为那会引起冲突,编译会报错的。同时,如果你把所有的实例变量都加一个下划线,你的同事看你的代码的时候,也马上就知道,这是一个实例变量,我使用时得小心。当然,还有apple的kvc和kvo机制,也靠下划线去搜索key,具体我不展开说了,看书吧。
最后,注意,这里的getter和setter方法不是线程安全的,但是,对于本应用程序来说,getter和setter方法只会在主线程里面访问,所以“线程安全不安全”,跟咱没关系!
现在,你地基有了,开干吧!
现在,你有新的getter和setter了,修改本类中的其它代码,开始使用getter和setter吧。让我们先从sushiTypes开始:
// In viewDidLoad self.sushiTypes = [[[NSArray alloc] initWithObjects:@"California Roll", @"Tuna Roll", @"Salmon Roll", @"Unagi Roll", @"Philadelphia Roll", @"Rainbow Roll", @"Vegetable Roll", @"Spider Roll", @"Shrimp Tempura Roll", @"Cucumber Roll", @"Yellowtail Roll", @"Spicy Tuna Roll", @"Avocado Roll", @"Scallop Roll", nil] autorelease]; // In viewDidUnload and dealloc self.sushiTypes = nil;
调用“self.sushiTypes = xxx”和调用 “[self setSushiTypes:xxx]”,这两者完全等价--这里的“.”号,对于我来说,就是“好看”而已。
因此,我们不是去直接访问_sushiTypes实例变量,而是用setter来设置它的值。回想一下,setter会把传入的对数引用计数加1.因此,现在,我们不能直接把alloc/init得到的值直接赋给_sushiTypes了(因为,我们通过setter访问的时候,那这个alloc/init创建的变量的引用计数会是2,那会有问题,因为它的所有者只有一个,那意味着,在将来,只会被所有者release一次。但是,此时引用计数还是1,永远也不会被释放掉了。也就是说,恭喜你!内存泄露了!)所以,在调用alloc/init之后,我们还需要调用一下autorelease。
在viewDidUnload和dealloc方法里同,我们不再是手动地relase再设置为nil了。我们只需要使用setter,一句设计self.xxx = nil就搞定。如果你用self._sushiTypes = nil的话,那么会生成下列的代码:
[nil retain]; // Does nothing [_sushiTypes release]; _sushiTypes = nil;
顺便说一下,给您提个醒----一些人可能跟你说过“永远不要在init或者dealloc方法里面使用getter或者setter方法”。他们这样说是为什么呢?因为,如果你在alloc或者dealloc函数里面使用setter或getter,但是,它的子类重写了getter和setter,因为objc所有的方法都是“虚方法”,也就是说可以被重写。那么子类init方法调用[(self = [super init]))的时候,先调父类的init方法,而里面使用了getter和setter,而正好这两个方法又被你覆盖了,如果你在这两个覆盖的方法里面干了一些事,那么就会有问题了。仔细想想,为什么!因为,你的子类还没有初使化完毕啊!!!你现在还在调用父类的“构造函数”,但是,你已经使用了子类的方法!!!但是,我想说的是,我在这里违反了某些人提供的原则。为什么,因为我知道会可能有副作用。所以,我不会轻易重载父类的getter或setter方法。我们这里这样写,可以帮助我们简化代码。这当然是我个人意见,仅供参考。
现在,修改代码,让lastSushiSelected也使用setter方法来赋值:
self.lastSushiSelected = sushiString; // In dealloc self.lastSushiSelected = nil;
哇---现在,我们对于内存问题的担心少了很多了,不是吗?你没有开动脑筋使劲想,哪里需要retain啊,哪些需要release啊。这里的setter方法,它在某种程度上替你完成了内存管理的工作。
一个简单的建议
因此,写getter和setter可以方便其它类访问你类里面的实例变量,同时,有时候也会使你的内存管理工作变得更加轻松。
但是,一遍又一遍地写一大堆这些getter和setter方法,那么我会疯掉的。搞java的为什么没疯?因为eclipse自动可以生成。搞c++的为什么也没疯,因为,可以直接public。但是,objc 的@public是没用的。不用担心,没有人会想一遍又一遍地干重复的事情的,所以objc2.0提供了一个新的,非常有用的特性,叫做@property,也叫属性。
我们可以把我们前面写的那些getter和setter方法全部注释掉,只需要写上下面两行代码就够了。自己动手试一下吧,打开RootViewController.h,然后找到getter和setter声明的地方,把它换成下面的2行代码:
@property (nonatomic, retain) NSArray * sushiTypes;
@property (nonatomic, retain) NSString * lastSushiSelected;
这是使用属性的第一步,创建属性声明。
属性的声明以@property关键字开始,然后在括号里面传入一些参数(比如atomic/nonatomic/assign/copy/retain等)。最后,你指明了属性的类型和名字。
propetyceov是一些特殊的关键字,它可以告诉编译器如何生成getter和setter。这里,你指定了两个参数,一个是nonatomic,它是告诉编译器,你不用担心多线程的问题。还有一个是reatin,它是告诉编译器,在把setter参数传给实例变量之前,要先retain一下。
在其它情况下,你可能想使用“assign”参数,而不是reatin,“assign”告诉编译器不要retain传入的参数。或者,有时你还需要指定“copy”参数,它会在setter参数赋值给实例变量之前,先copy一下。
好了,为了完成property的使用,你先转到RootViewController.m,删除之前写的getter和setter,然后在文件的顶部添加下面2行代码:
@synthesize sushiTypes = _sushiTypes; @synthesize lastSushiSelected = _lastSushiSelected;
上面的代码是告诉编译器,请你基于我前面定义的property及其参数,请为我生成相应的getter和setter方法。你使用@synthesize关键字开始,然后给出属性名字,(如果属性名字和实例变量名字不一样的话),那么一定要写上=于号,这样在生成setter方法的时候,编译才知道,传入的参数要赋值给谁。切记一次要写上等于号!!!如果实例变量名和属性名一样,那就没必须了。
就这么多!编译并运行代码吧,一样很ok,运行得很好。但是,和之前你写的那堆代码相比较,是不是更容易理解了呢?同时也会使得出错的概率下降。
到目前为止,你应该了角propety的用法,以及具体是如何工作的吧!接下来,我将给出一些使用property的建议。
一般性的策略
我想在这篇教程里面添加一些这样的策略,因为,它能够帮助我在管理objc内存的时候,更加轻松,更加不容易犯错误。
如果你遵守这些规则的话,那么,在大部分时候,你会远离内存相关问题的烦恼。当然,盲目地背下这些规则而不去理解,为什么会有这些规则,为什么这些规则就能够有作用。这肯定是不行的啦!但是,如果你是新手的话,你可以按照我给的这些规则去做,这样你会避免大量的内存相关的错误,使得你的日常编程活动更加轻松。
我先把这些规则一条条列出来,接下来,我再详细依个讨论。
- 总是为所有的实例变量定义属性。
- 如果它是一个类,那么就设定“retain”为属性参数,否则的话,就设置为assign。
- 任何时候创建一个类的实例,请使用alloc/init/autorelease 的方式创建。
- 任何时候,当给一个变量赋值的时候,总是使用e “self.xxx = yyy”。换句话说,就是使用property。
- 对于你的每一个实例变量,在dealloc函数里面调用 “self.xxx = nil”。如果是一个outlet的话,那么在viewDidLoad里面创建,要记得在viewDidUnload里面销毁。
好,现在开始逐条讨论!
规则1:通过为每一个实例变量定义property,你可以让编译器为你写内存相关的代码。缺点很明显了,你破坏了类的封装性,这样的话,可能会使你的类的耦合度变得更高,更不利用维护和代码复用。
规则2:通过把类的property参数指明为retain,那么在任何时候,你可以都可以访问它们。你们你还保存有它们的一个引用计数,内存不会被释放掉的,你是拥有者,你负责释放。
规则3:当你创建一个类对象的时候,使用alloc/init/autorelease惯用法(就像你之前创建sushiTypes的数组那样的)。这样的话,内存会被自动释放。如果你想让它不释放的话,那么在要赋值的实例变量,在声明其property的参数那里,声明一个retain吧。
规则4:不管什么时候给实例变量赋值,都使用 self.xxx的语法,这样的话,当你给实例变量赋值的时候,会先把老的值释放掉,并且retain新的变量值。注意,有些程序员担心在init和dealloc函数里面使用getter和setter函数会带来副作用,但是,我认为这没什么。只要你对内存管理规则完全清楚,你不会去做“在子类里重写父类的getter和setter”方法的事的。或者,就算实际需要,必须重写父类的getter和setter,你也会十分注意,不在重写的过程中,给代码带来任何副作用的,对吧?
规则5:在dealloc函数里面,使用“self.xxx = nil” ,这样可以通过property使用其引用计数减1.不用要忘了还有viewDidUnload!
与cocos2d有关的简单策略
我知道,现在我的博客上有一大批cocos2d的忠实粉丝,因此,接下来的tips是特意为你们准备的!
上面提出的5点规则,对于coocs2d来说,有点太严格了,或者直接说,太死了。因为,大部分时候,我们的对象都加到层里面去了,我们在类里面定义一些实例变量,仅仅是为了在除init方法之外的其他方法里面方法使用。(其实,很多人喜欢定义tag,然后在addChild的时候指定一个tag,然后在其他方法里同,使用[self getChildByTag:xxx]来获得你想要的对象。因为层里面有个CCArray的数组,它用来保存层的所有孩子结点,当调用addChild的时候,其实是调用CCArray的addObject方法,所以,加到层里面的孩子,其引用计数会加1.
因此,为了避免定义一些不必要的property,下面是我对于cocos2d的使用者的一些建议:
- 从来不使用property。
- 把你创建的sprite实例直接赋值给你定义的实例变量。
- 因为这些精灵都会加到当前层里面去,coocs2d会自动retain,使其引用计数加1.
- 当你把一个对象从当前层中移除出去的时候,记得把它赋值为nil。
我个人觉得用上面4条方法来开发cocos2d游戏,感觉还不错,简单,快捷。
注意,如果某个对象没有加到当前层里面去,比如action。那么,就不会使用上面4个规则了。手动去reatin/release吧。
记住,规则是死的,人是活的!只要理解了objc的内存管理规则,你可以忘记上面所有的规则!
何去何从?
这里有本教程的完整源代码。
如果你还对property或者内存管理方面有任何疑问的话,请留言。当然,如果各位看观有什么好的,关于内存管理的小诀窍,小技巧,也欢迎提出来,分享一下,在下十分感激!
目前为止,关于objc的内存管理系列教程就全部结束啦。真心希望,通过翻译这3篇教程,以及我自己写的那一篇教程,能够帮助大家走出objc内存管理的泥潭。
如果有什么好的意见或者建议,请在下方留言。如果您想希望得到哪方面的教程,或者你对哪方面还不太熟悉,也请留言。虽然我很忙(其实大家都很忙:)),但是,我有时间的时候,还是会尽力满足大家的要求的。
再次谢谢您的阅读,下篇教程见!
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!