(譯)在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記憶體管理的泥潭。
如果有什麼好的意見或者建議,請在下方留言。如果您想希望得到哪方面的教程,或者你對哪方面還不太熟悉,也請留言。雖然我很忙(其實大家都很忙:)),但是,我有時間的時候,還是會儘力滿足大家的要求的。