Objective-C屬性(property)的特性(attribute)

先以一圖總結(jié):

Atttributes

以下:「attribute(s)」,「特性」是指同一事物(都指@property后面括號(hào)內(nèi)的單詞)。

用Objective-C做過開發(fā)的朋友都知道,類里面的屬性(可以近似地理解為類的變量)是用@property關(guān)鍵字定義的,然后@property后面的括號(hào),會(huì)寫上若干「特性(attribute)」,后面跟數(shù)據(jù)類型、屬性名稱。如:

@property (copy, nonatomic) NSString *name;

寫OC良久,對(duì)括號(hào)內(nèi)的這些attributes,還是一知半解、不知其然,亦不知其所以然。用的時(shí)候就照葫蘆畫瓢。

現(xiàn)在大伙兒慢慢轉(zhuǎn)向蘋果的新開發(fā)語言Swift,似乎亦不必花太多時(shí)間在Objective-C上。

不過那種一知半解,不明就里的感覺,有點(diǎn)如鯁在喉,不甚舒服,所以花了點(diǎn)時(shí)間,research了一番:

為什么要有@property?

要搞清楚「特性」,先搞清楚@property,為什么要有@property

在2006年的WWDC大會(huì)上,蘋果發(fā)布了Objective-C 2.0,其中就包括Properties這個(gè)新的語法,把原來的實(shí)例變量定義成Properties(屬性)。這個(gè)變化,和以前相比,有什么變化呢?

Objective-C2.0之前:

沒有Properties之前,定義實(shí)例變量,是這樣的:

@interface Person : NSObject {
@public
    NSString *name;
@private
    int age;
}
@end

然后在.h文件,聲明setter和getter方法(setter和getter統(tǒng)稱「accessors/存取器/訪問器」),再在.m文件實(shí)現(xiàn)setter和getter,這樣就可以封裝起來,供其他類訪問(取值、賦值)了。

然而,即使不使用setter和getter,其他類也可以通過->來直接訪問,如:

 personA->name = @"123";
    
 NSLog(@"personA->name:%@", personA->name);

為什么要getter和setter

那么,為什么還要如此麻煩地聲明和實(shí)現(xiàn)setter和getter呢?主要基于三個(gè)原因(參考:Please explain Getter and Setters in Objective C):

  • 可以在getter和setter中添加額外的代碼,實(shí)現(xiàn)特定的目的。比如賦值前(set)需要實(shí)現(xiàn)一些特定的內(nèi)部計(jì)算,或者更新狀態(tài),緩存數(shù)據(jù)等等。
  • KVC和KVO都是基于此實(shí)現(xiàn)的。
  • 在非ARC時(shí)代,可以在在getter和setter中進(jìn)行內(nèi)存管理。

因此,寫getter和setter,可算是Objective-C中「約定俗成」的做法了。(Swift有類似的「Computed Properties/計(jì)算屬性」)

所以,在沒有Objective-C2.0@property之前,我們幾乎需要為所有的實(shí)例變量,手動(dòng)寫getter和setter——聽聽就覺得很可怕,對(duì)不對(duì)?

Objective-C2.0之后:

慶幸的是,程序員都喜歡「偷懶」,所以就有了2006年Objective-C2.0中的新語法:Properties

它幫我們自動(dòng)生成getter和setter(聲明方法,并實(shí)現(xiàn)方法。當(dāng)然,這部分代碼并不會(huì)出現(xiàn)在你的項(xiàng)目中,是隱藏起來的)。

不過,@property的寫法,也經(jīng)過數(shù)次變遷(新舊寫法混在一起,就更讓人困惑了):

  • 最開始,需要作3件事情:
    • 在.h文件,我們用@property聲明了屬性——這只是幫我們?cè)诼暶髁薵etter和setter;
    • 還需要手動(dòng)聲明實(shí)例變量(和Objective-C2.0之前一樣)
    • 然后在.m文件,還要用@synthesize自動(dòng)合成getter和setter的實(shí)現(xiàn)。
  • 后來,不需要為屬性聲明實(shí)例變量了,@synthesize會(huì)默認(rèn)自動(dòng)生成一個(gè)「下劃線+屬性名」的實(shí)例變量。比如@property (copy, nonatomic) NSString *name;之后,就可以直接使用_name這個(gè)變量了。
  • 再后來(Xcode4.5開始),@synthesize也不需要了。一個(gè)@property搞定。

所以,現(xiàn)在我們寫@property聲明屬性,其實(shí)是做了三件事

  • .h: 聲明了getter和setter方法;
  • .h: 聲明了實(shí)例變量(默認(rèn):下劃線+屬性名);
  • .m: 實(shí)現(xiàn)了getter和setter方法。

這就是@property為我們所做的事情。

知道它為我們做了什么,自然也就能回答:「為什么要有@property?」這個(gè)問題了。

@property后面的括號(hào)又是怎么回事?

@property (copy, nonatomic) NSString *name;

這種寫法,大家肯定都寫過,不過,后面跟著的這個(gè)括號(hào)又是什么玩意兒呢?

官方把括號(hào)里面的東西,叫做「attribute/特性」。

先試一下,把括號(hào)里的兩個(gè)單詞都刪掉,你會(huì)發(fā)現(xiàn),還能正常工作。而事實(shí)上,以下兩種寫法,是等價(jià)的:

@property () NSString *name;// 或者@property NSString *name;
@property (atomic, strong, readwrite) NSString *name;

因?yàn)閍ttribute主要有三種類型(實(shí)際上最多可以寫6個(gè)特性,后面詳述),每種類型都有默認(rèn)值。如果什么都不寫,系統(tǒng)就會(huì)取用默認(rèn)值(看看,蘋果良苦用心,偷偷幫我們做了那么多事情)。

如上所述,attributes有三種類型:

1.Atomicity(原子性)

比較簡單的一句話理解就是:是否給setter和getter加鎖(是否保證setter或者getter的每次訪問是完整性的)。

原子性,有atomic和nonatomic兩個(gè)值可選。默認(rèn)值是atomic(也就是不寫的話,默認(rèn)是atomic)。

  • atomic(默認(rèn)值)

使用atomic,在一定程度上可以保證線程安全,「atomic的作用只是給getter和setter加了個(gè)鎖」。也就是說,有線程在訪問setter,其他線程只能等待完成后才能訪問。

它能保證:即使多個(gè)線程「同時(shí)」訪問這個(gè)變量,atomic會(huì)讓你得到一個(gè)有意義的值(valid value)。但是不能保證你獲得的是哪個(gè)值(有可能是被其他線程修改過的值,也有可能是沒有修改過的值)。

  • nonatomic

而用nonatomic,則不保證你獲得的是有效值,如果像上面所述,讀、寫兩個(gè)線程同時(shí)訪問變量,有可能會(huì)給出一個(gè)無意義的垃圾值。

這樣對(duì)比,atomic就顯得比較雞肋了,因?yàn)樗⒉荒芡耆WC程序?qū)用娴木€程安全,又有額外的性能耗費(fèi)(要對(duì)getter和setter進(jìn)行加鎖操作,我驗(yàn)證過,在某個(gè)小項(xiàng)目中將所有的nonatomic刪除,內(nèi)存占用平均升高1M左右)。

所以,你會(huì)見到,幾乎所有情況,我們都用nonatomic。

2.Access(存取特性)

存取特性有readwrite(默認(rèn)值)和readonly

這個(gè)從名字看就很容易理解,定義了這個(gè)屬性是「只讀」,還是「讀寫」皆可。

如果是readwrite,就是告訴編譯器,同時(shí)生成getter和setter。如果是readonly,只生成getter。

3.Storage(內(nèi)存管理特性)(管理對(duì)象的生命周期的)

最常用到strongweakassigncopy4個(gè)attributes。(還有一個(gè)retain,不怎么用了)

  • strong (默認(rèn)值)

ARC新增的特性。

表明你需要引用(持有)這個(gè)對(duì)象(reference to the object),負(fù)責(zé)保持這個(gè)對(duì)象的生命周期。

注意,基本數(shù)據(jù)類型(非對(duì)象類型,如int, float, BOOL),默認(rèn)值并不是strong,strong只能用于對(duì)象類型。

  • weak

ARC新增的特性。

也會(huì)給你一個(gè)引用(reference/pointer),指向?qū)ο蟆5遣粫?huì)主張所有權(quán)(claim ownership)。也不會(huì)增加retain count。

如果對(duì)象A被銷毀,所有指向?qū)ο驛的弱引用(weak reference)(用weak修飾的屬性),都會(huì)自動(dòng)設(shè)置為nil。

在delegate patterns中常用weak解決strong reference cycles(以前叫retain cycles)問題。

  • copy

為了說明copy,我們先舉個(gè)栗子:

我在某個(gè)類(class1)中聲明兩個(gè)字符串屬性,一個(gè)用copy,一個(gè)不用:

@property (copy, nonatomic) NSString *nameCopy;

// 或者可以省略strong, 編譯器默認(rèn)取用strong
@property (strong, nonatomic) NSString *nameNonCopy;

在另一個(gè)類中,用一個(gè)NSMutableString對(duì)這兩個(gè)屬性賦值并打印,再修改這個(gè)NSMutableString,再打印,看看會(huì)發(fā)生什么:

Class1 *testClass1 = [[Class1 alloc] init];

NSMutableString *nameString = [NSMutableString  stringWithFormat:@"Antony"];

// 用賦值NSMutableString給NSString賦值
testClass1.nameCopy = nameString;
testClass1.nameNonCopy = nameString;
   
NSLog(@"修改nameString前, nameCopy: %@; nameNonCopy: %@", testClass1.nameCopy, testClass1.nameNonCopy);

[nameString appendString:@".Wong"];
   
NSLog(@"修改nameString后, nameCopy: %@; nameNonCopy: %@", testClass1.nameCopy, testClass1.nameNonCopy);

打印結(jié)果是:

修改nameString前, nameCopy: Antony; nameNonCopy: Antony
修改nameString后, nameCopy: Antony; nameNonCopy: Antony.Wong

我只是修改了nameString,為什么testClass1.nameNonCopy的值沒改,它也跟著變了?

因?yàn)?code>strong特性,對(duì)對(duì)象進(jìn)行引用計(jì)數(shù)加1,只是對(duì)指向?qū)ο蟮闹羔樳M(jìn)行引用計(jì)數(shù)加1,這時(shí)候,nameStringtestClass1.nameNonCopy指向的其實(shí)是同一個(gè)對(duì)象(同一塊內(nèi)存),nameString修改了值,自然影響到testClass1.nameNonCopy

copy這個(gè)特性,會(huì)在賦值前,復(fù)制一個(gè)對(duì)象,testClass1.nameCopy指向了一個(gè)新對(duì)象,這時(shí)候nameString怎么修改,也不關(guān)它啥事了。應(yīng)用copy特性,系統(tǒng)應(yīng)該是在setter中進(jìn)行了如下操作:

- (void)setNameCopy:(NSString *)nameCopy {
    _nameCopy = [nameCopy copy];
}

大家了解copy的作用了吧,是為了防止屬性被意外修改的。那什么時(shí)候要用到copy呢?

所有有mutable(可變)版本的屬性類型,如NSString, NSArray, NSDictionary等等——他們都有可變的版本類型:NSMutableString, NSMutableArray, NSMutableDictionary。這些類型在屬性賦值時(shí),右邊的值有可能是它們的可變版本。這樣就會(huì)出現(xiàn)屬性值被意外改變的可能。所以它們都應(yīng)該用copy

擴(kuò)展

如果不用copy,而是在賦值前,調(diào)用copy方法,可以達(dá)到同樣的目的:

// 這時(shí)候也可以確保nameNonCopy不會(huì)被意外修改
testClass1.nameNonCopy = [nameString copy];

如果用copy修飾NSMutableString、NSMutableArray會(huì)發(fā)生什么?

如果用copy修飾NSMutableString,在賦值的時(shí)候會(huì)報(bào)如下警告:

Incompatible pointer types assigning to 'NSMutableString *' from 'NSString *'

而如果用copy修飾NSMutableArray,則在調(diào)用addObject:時(shí)直接crash:

reason: '-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x1700045c0'

如果理解了「copy特性,就是在setter中,進(jìn)行了copy操作」,就很容易知道以上報(bào)錯(cuò)的原因:屬性在賦值時(shí),調(diào)用setter,已經(jīng)將原本mutable的對(duì)象,copy成了immutable的對(duì)象(NSMutableString變成NSString,NSMutableArray變成NSArray)。

  • assign

是非ARC時(shí)代的特性,

它的作用和weak類似,唯一區(qū)別是:如果對(duì)象A被銷毀,所有指向這個(gè)對(duì)象A的assign屬性并不會(huì)自動(dòng)設(shè)置為nil。這時(shí)候這些屬性就變成野指針,再訪問這些屬性,程序就會(huì)crash。

因此,在ARC下,assign就變成用于修飾基本數(shù)據(jù)類型(Primitive Type),也就是非對(duì)象/非指針數(shù)據(jù)類型,如:int、BOOL、float等。

注意,在非ARC時(shí)代,還沒有strong的時(shí)候。assign是默認(rèn)值。ARC下,默認(rèn)值變成strong了。這個(gè)要注意一下,否則會(huì)引起困擾。

  • retain

retain是以前非ARC時(shí)代的特性,在ARC下并不常用。

它是strong的同義詞,兩者功能一致。不知道為什么還保留著,這對(duì)新手也會(huì)造成一定困擾。

所以,總結(jié)一下。

  • 幾乎所有情況,都寫上nonatomic
  • 對(duì)外「只讀」的,寫上readonly
  • 一般的對(duì)象屬性,寫上strong(用retain也可以,比較少用)
  • 需要解決strong reference cycles問題的對(duì)象屬性,strong改為weak
  • 有mutable(可變)版本的對(duì)象屬性,strong改為copy
  • 基本數(shù)據(jù)類型(int, float, BOOL)(非對(duì)象屬性),用assign

4.擴(kuò)展

其實(shí),除了上面3種經(jīng)常用到的特性類型,還有2種不太見到。

  • getter=setter=

按字面意思,很容易理解,就是重命名getter和setter方法。

Transitioning to ARC Release Notes中寫道:

You cannot give an accessor a name that begins with new. This in turn means that you can’t, for example, declare a property whose name begins with new unless you specify a different getter

存取方法不能以new開頭,如果你要以new開頭命名一個(gè)屬性:@property (copy, nonatomic) NSString *newName;于是會(huì)默認(rèn)生成一個(gè)new開頭的getter方法:

這時(shí)候就會(huì)報(bào)錯(cuò):Property follows Cocoa naming convention for returning 'owned' objects

解決辦法,就是用getter=重命名getter方法:

@property (copy, nonatomic, getter=theNewName) NSString *newName;

  • Nullability
    • nullable:對(duì)象「可為空」
    • nonnull:對(duì)象「不可為空」
    • null_unspecified:「未指定」
    • null_resettable:稍有點(diǎn)難理解,就是調(diào)用setter去reset屬性時(shí),可以傳入nil,但是getter返回值,不為空。UIView下面的tintColor,就是null_resettable。這樣就保證,即使賦值為nil,也會(huì)返回一個(gè)非空的值。

為了更好地和Swift混編(配合Swift的optional類型),在Xcode 6.3,Objective-C新增了一個(gè)語言特性,nullability。具體就是以上4個(gè)新特性。

如果設(shè)置為null_resettable,則要重寫setter或getter其中之一,自己做判斷,確保真正返回的值不是nil。否則報(bào)警告:Synthesized setter 'setName:' for null_resettable property 'name' does not handle nil

Nullability的寫法如下:

@property (copy, nullable) NSString *name;
@property (copy, readonly, nonnull) NSArray *allItems;

// 也可以將nullable, nonnull, null_unspecified, null_resettable三個(gè)修飾語前面加雙下劃線,用于修飾指針、參數(shù)、返回值等(null_resettable只能在屬性括號(hào)中使用)
@property (copy, readonly) NSArray * __nonnull allItems;

Nullability的默認(rèn)值:null_unspecified——未指定。如果某個(gè)屬性填寫了Nullability特性(比如寫了nonnull),沒有填寫Nullability的屬性,會(huì)出現(xiàn)如下警告:

Pointer is missing a nullability type specifier (_Nonnull, _Nullable, or _Null_unspecified)

但是如果每個(gè)屬性都一一寫上,稍嫌麻煩。而因?yàn)榇蠖鄶?shù)屬性是nonnull的,所以蘋果定義了兩個(gè)宏,NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END(兩個(gè)宏之間,叫做Audited Regions)。

將所有屬性包在這兩個(gè)宏中,就無需寫nonnull修飾語了,只需要在「可為空」的屬性里,寫上nullable即可:

NS_ASSUME_NONNULL_BEGIN
@interface AAPLList : NSObject <NSCoding, NSCopying>
// 只需要為「不可為空」的參數(shù)、屬性、返回值加上修飾語nullable即可
- (nullable AAPLListItem *)itemWithName:(NSString *)name;
- (NSInteger)indexOfItem:(AAPLListItem *)item;

@property (copy, nullable) NSString *name;
@property (copy, readonly) NSArray *allItems;
// ...
@end
NS_ASSUME_NONNULL_END

所以!綜上所述,attribute最多可以寫6個(gè)進(jìn)去:1.原子性、2.存取特性、3.內(nèi)存管理特性、4.重命名getter、5.重命名setter,6.nullability:

@property (nonatomic, readonly, copy, getter=theNewTitle, setter=setTheNewTitle:, nullable) NSString *newTitle;

不過,應(yīng)該沒有誰閑得蛋疼會(huì)這樣寫的。

最短的寫法就是什么都不寫,連括號(hào)都可以不要:

@property BOOL isOpen;

畢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,629評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評(píng)論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,139評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,411評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,641評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,820評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,362評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,604評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容