來源于 Ry’s Objective-C Tutorial - RyPress
一個學習Objective-C基礎知識的網站.
個人覺得很棒,所以決定抽時間把章節翻譯一下.
本人的英語水平有限,有讓大家誤解或者迷惑的地方還請指正.
原文地址:http://rypress.com/tutorials/objective-c/properties.html
僅供學習,如有轉摘,請注明出處.
一個對象的屬性允許其他對象查看或者修改它的狀態.但是,在一個良好的面向對象設計中,是不可能(允許)直接獲取一個對象的內部狀態.而是通過存取器(getters與setters)與對象的內部數據進行交互.

@property指令的目的是通過自動生成存取器方法來方便的創建,配置屬性.它允許你在語義上指定公有屬性的行為,并且不用你操心實現細節.
這個模塊概覽這些不同的允許你變更getters與setters行為的特性.其中的一些特性決定了屬性如何控制(管理)自己內存,所以該模塊也是對OC內存管理的一個實用介紹.對于(這方面)更詳細的討論,請參閱內存管理.
@property指令
首先,讓我們先看一下當使用@property指令時到底發生了什么事情.請看下面的一個簡單Car類的接口和對應實現.
// Car.h
#import <Foundation/Foundation.h>
@interface Car : NSObject
@property BOOL running;
@end
// Car.m
#import "Car.h"
@implementation Car
@synthesize running = _running; // Optional for Xcode 4.4+
@end
編譯器為running屬性(分別)生成了getter和setter.默認的命名規約是使用其屬性名稱作為getter,在屬性名之前加上set作為setter,其對應的實例變量則是在屬性名前加上下劃線,就像這樣:
- (BOOL)running {
return _running;
}
- (void)setRunning:(BOOL)newValue {
_running = newValue;
}
在你通過@property指令聲明了屬性之后,你就能調用這些方法,好像(這些方法)已經被包含在你的類接口和實現文件中.你也可以在Car.m中重寫它們以提供自定義的getter/setter,這樣的話就得必須使用@synthesize指令.然而,你應該沒有必要要自定義存取器,盡管@property指令允許在抽象的層級上這樣做.
通過點語法訪問屬性實際上是轉換成通過上述的存取器方法,所以下面(代碼塊)的honda.running代碼在給其賦值時實際上是調用setRunning:在讀取它的值時是調用running方法:
// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Car *honda = [[Car alloc] init];
honda.running = YES; // [honda setRunning:YES]
NSLog(@"%d", honda.running); // [honda running]
}
return 0;
}
為了更改已生成的存取器的行為,你可以在@property指令后的小括號內指定其特性.下面的模塊將介紹這些可用的特性.
getter= 與 setter= 特性
如果你不喜歡@property這種默認的命名規約,你可以使用getter=與setter=特性來更改getter/setter的方法名.對于Boolean屬性,常用的getter方法名是(在其屬性名)添加is前綴.將Car.h的屬性聲明調整成下面這樣:
@property (getter=isRunning) BOOL running;
此時生成的存取器就是isRunning以及setRunning了.注意,公有屬性(名稱)仍然是running,這也是你在點語法中該使用的名稱:
Car *honda = [[Car alloc] init];
honda.running = YES; // [honda setRunning:YES]
NSLog(@"%d", honda.running); // [honda isRunning]
NSLog(@"%d", [honda running]); // Error: method no longer exists
這是唯一一個需要參數的屬性特性(用于標識存取器方法) - 其他的都是boolean標識.
只讀特性
只讀特性讓你方便地將一個屬性變成只讀的.它會忽略setter方法,并會阻止通過點語法(給其)賦值,但是對getter沒有影響.作為例子,將Car接口修改如下,注意我們怎么使用逗號分隔來給屬性指定多個特性.
#import <Foundation/Foundation.h>
@interface Car : NSObject
@property (getter=isRunning, readonly) BOOL running;
- (void)startEngine;
- (void)stopEngine;
@end
現在,我們將通過startEngine和stopEngine方法在內部設置running屬性,而不是讓其他對象修改.對應的實現如下.
// Car.m
#import "Car.h"
@implementation Car
- (void)startEngine {
_running = YES;
}
- (void)stopEngine {
_running = NO;
}
@end
請記住,@property也為我們生成了一個實例變量,這是我們為什么在不聲明的情況下,也可以使用_running的原因(在此處,我們不能使用self.running因為它是只讀的).我們在main.m中添加如下代碼來測試新的Car類.
Car *honda = [[Car alloc] init];
[honda startEngine];
NSLog(@"Running: %d", honda.running);
honda.running = NO; // Error: read-only property
到此(來看),屬性是一個避免我們自己(根據樣板)寫getter/setter方法的快捷方式.(與readonly相比),這種情況不適用于其余的那些能顯著變更屬性行為的特性.它們只適用于存儲OC對象的屬性(并非C語言的基本類型).
非原子特性
原子性必須處理線程環境下的屬性行為.當有多個線程時,有可能會同時調用setter與getter方法.這也就意味著getter/setter會被其他操作打斷,也就可能會產生壞數據.
原子屬性通過鎖定一個基礎的(普通的)對象來阻止這種事情的發生,并保證get/set操作的完整性(數據完整性).然而,使用原子屬性只是線程安全的一個方面,并不意味著你的代碼是線程安全的,理解這一點很重要.
@property聲明的屬性默認是原子特性,這會產生一些管理成本(開銷).所以,如果處在一個非多線程的環境(或者你自己實現了線程安全),你肯定會用nonatomic特性來重寫(變更)這個行為,像這樣:
@property (nonatomic) NSString *model;
這有一個關于atomic特性小的,實用的警告.對于具有原子特性的屬性,它的存取器要么自己生成,要么自定義.只有non-atomic特性的屬性才允許混合-既可使用synthesized,又可以自定義.可以通過移除上面代碼中的nonatomic,并在Car.m增加一個自定義的getter來驗證這一點.
內存管理
在任何一種OOP(objected-oriented Pragram)語言中,對象都駐留在電腦內存中,在移動設備上- 這些(內存)是稀缺資源.內存管理的目標通過一種高效的創建,銷毀對象的方式來保證程序不會占用超出它們所需(內存)空間的額外空間.
很多編程語言通過垃圾回收機制來實現(內存管理),而OC使用另一種更高效的,被稱作object ownership的機制.當你與一個對象開始進行交互時,你便擁有了那個對象,也就意味著這種關系(擁有該對象)能保證在你使用期間,該對象一直存在.當你不在使用,你便打破(放棄)這種擁有關系,此時,如果該對象沒有其他擁有者,系統便會銷毀該對象,并釋放其內存.

隨著自動引用計數的出現,編譯器會幫自動給幫你管理這些擁有關系.意味著大多數情況下,你都不用操心內存管理如何工作.但是,你還是都明白屬性的strong,weak與copy特性,因為它們告訴編譯器這種對象關系屬于哪一種.
strong特性
strong特性下,無論給屬性分配什么樣的對象,都會創建一個擁有關系.這對所有的對象屬性來說都是明確地行為,這樣就保證了只要屬性被賦值,那值就會存儲(存在).
我們創建另一個Person類來看一下上訴的工作原理.該類的接口僅聲明了一個name屬性:
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic) NSString *name;
@end
下面是實現.它使用@property默認生成的存取器.還重寫了NSObject的description方法,該方法是用來返回對象的字符串描述.
// Person.m
#import "Person.h"
@implementation Person
- (NSString *)description {
return self.name;
}
@end
接下來,我們在Car類中添加一個Person屬性,修改Car.h如下.
// Car.h
#import <Foundation/Foundation.h>
#import "Person.h"
@interface Car : NSObject
@property (nonatomic) NSString *model;
@property (nonatomic, strong) Person *driver;
@end
然后,調整(又一次出現的)main.m的代碼:
// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *john = [[Person alloc] init];
john.name = @"John";
Car *honda = [[Car alloc] init];
honda.model = @"Honda Civic";
honda.driver = john;
NSLog(@"%@ is driving the %@", honda.driver, honda.model);
}
return 0;
}
因為driver是一個strong關系(強引用),所以handa對象擁有john.這樣就保證了john(對象)只要在honda對象需要,它就一直有效(存在).
weak特性
大多數情況,在設置對象屬性時,第一直覺都是給它strong特性.然而,strong引用(在一些情況下)會引起一個問題.舉個例子,我們需要一個對Car對象的引用來標識人正在開車.首先,我們在Person.h中增加一個car屬性.
// Person.h
#import <Foundation/Foundation.h>
@class Car;
@interface Person : NSObject
@property (nonatomic) NSString *name;
@property (nonatomic, strong) Car *car;
@end
@class Car這一行是對Car類的向前聲明.就像是在告訴編譯器,"相信我,Car類肯定存在,不用現在去加載它."我們必須用這種方式來替代我們常用的#import語句,因為Car也可以導入Person.h,這樣我們就會陷入一個無盡的導入循環.(編譯器可不喜歡無盡的循環.)
接下來,在main.m中,給honda.driver分配值之后加入下面一行代碼:
honda.driver = john;
john.car = honda; // Add this line
現在,我們有一個honda擁有john的關系,也有一個jhon擁有honda的關系.這意味著兩個對象相互擁有(引用),導致內存管理系統無法銷毀它們,即使這兩個對象不再需要了.

這被稱作是retain cycle,一種內存泄露的形式,內存泄露are bad.幸運的是,很容易修復這個問題-只要告訴其中的一個屬性對另外一個對象保持一個弱引用即可.在Person.h中,將car的聲明調整如下:
@property (nonatomic, weak) Car *car;
weak特性創建一個對car的非擁有關系.這樣就允許john在避免retain cycle的情況下,仍存在一個對honda的引用.這意味,很可能存在(下述的)這種情況,honda已經被銷毀了,而john仍有一個對honda的引用.這不應該發生,weak特性會設置car為nil以避免指針懸掛.

一個常用weak特性的情況是在父子數據結構中.按照規約,父對象應該保持對子對象的強引用,子對象則保持對父對象的弱引用.弱引用也是代理設計模式中與生俱來部分.
(上述的)關鍵點在于,兩個對象永遠不要互相強引用.weak特性讓在不產生retain cycle的情況下保持一個循環引用的關系成為可能.
copy特性
相對于strong特性,copy特性可以作為替代(品).它不形成一個擁有關系,而是對任何分配給屬性的對象創建一個副本,然后對副本有擁有關系.只有遵從NSCopying 協議的對象才可以使用這個特性.
Properties that represent values (opposed to connections or relationships) are good candidates for copying. 比如,開發人員一般都會復制NSString屬性,而不是強引用它們:
// Car.h
@property (nonatomic, copy) NSString *model;
現在,無論你給model賦什么值,Car都將存儲一個嶄新的實例.如果你使用的是可變值,那么當(屬性)第一次被賦值時,它便是不變的了.* If you’re working with mutable values, this has the added perk of freezing the object at whatever value it had when it was assigned.*.可以通過下面的代碼證明:
// main.m
#import <Foundation/Foundation.h>
#import "Car.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Car *honda = [[Car alloc] init];
NSMutableString *model = [NSMutableString stringWithString:@"Honda Civic"];
honda.model = model;
NSLog(@"%@", honda.model);
[model setString:@"Nissa Versa"];
NSLog(@"%@", honda.model); // Still "Honda Civic"
}
return 0;
}
NSMutableString是NSString的一個子類,可以隨時編輯.如果model沒有對原始的實例創建一個副本,我們將會看到第二行NSLog()輸入的是改變后的字符串(Nissan Versa) .
其他特性
對于最新的OC程序(iOS5+),上述的@property特性是你應該需要(使用)的,但是這仍然有一些其他的(特性),你可能會在比較舊的庫或者文檔中遇到.
retain特性
retain特性是strong特性在手動引用計數中的版本,它有著同樣地效果:分配值時要求一個擁有關系.在自動引用計數環境下,你不應該再使用它.
unsafe_unretained 特性
有著unsafe_unretained特性的屬性在行為上與(有著)weak特性的屬性類似,但是如果被引用的對象被銷毀后,它們的值不會自動設為nil.你應該使用unsafe_unretained特性的唯一原因就是為了讓你的類與不支持weak屬性的代碼兼容.
assign特性
assign特性在給屬性賦值時不會執行任何一種內存管理要求.它是基本數據類型的默認行為,并且它曾是在iOS5之前實現弱引用的一種方式.與retain一樣,在最新的(當代)應用程序你不需要明確地使用它.
總結
這個模塊展示了@property所有的供選擇的特性,希望你對修改已生成的存取器方法感覺相對適應.請記住,這些特性的目的是幫助你關注什么樣的數據應該被記錄.(這些)特性被總結如下.
特性 | 描述 |
---|---|
getter= | Use a custom name for the getter method. |
setter= | Use a custom name for the setter method. |
readonly | Don’t synthesize a setter method. |
nonatomic | Don’t guarantee the integrity of accessors in a multi-threaded environment. This is more efficient than the default atomic behavior. |
strong | Create an owning relationship between the property and the assigned value. This is the default for object properties. |
weak | Create a non-owning relationship between the property and the assigned value. Use this to prevent retain cycles. |
copy | Create a copy of the assigned value instead of referencing the existing instance. |
現在,我們已經搞定了屬性,我們將深入了解OC類中的另一半:方法.我們將探討(與方法相關的)一切,從命名規約背后的怪癖(原因)到動態方法調用.
寫于15年09月06號