主題06:瞭解特性
主題07:從內部存取實例變數時,主要採用直接存取的方式
主題14:瞭解什麼是類別物件
主題18:優先採用不可變物件
主題27:使用類別延續類目隱藏實作細節
整理
主題06
一、Property 是什麼?
特性 (property),嗯…我完全不喜歡書中的解釋,也不喜歡「特性」這個中譯名。很不明確,所以筆者要引 OMG UML-Superstructure-formal 的定義:
這裡說明了: property 是一種結構特徵。也就是說,它不是一種特例於某個語言的功能。若把類別一分為二:不是資料(或稱「attribue」、「成員變數」),就是行為(或稱「method」或「成員函式」)。沒有別的東西了。那 property 是什麼呢?7.3.45 Property (from Kernel, AssociationClasses, Interfaces)
A property is a structural feature.
A property related to a classifier by ownedAttribute represents an attribute, and it may also represent an association end. It relates an instance of the class to a value or collection of values of the type of the attribute.
簡單講,property 是 attribute 的 subset。但是它不同於一般的 attribute,是因為它還具備有表示
association end 的職責。換句話說,property 是具備與其他類別建立關係的 attribute。這表示它有公開的性質。
在 Java 來說,宣告一個 private attribute 後,建立相對應的 getter/setter methods 就是讓該 attribute 變成一個 property 的例子。而 Objective-C 則是例用 @property 這個關鍵字讓 compiler 知道:這個 attribute 應該有對應的 getter/setter 實作。
所以對於 client end 來說,可以在得到某 instance 後,利用該 instance 提供的 getter/setter methods 對該 instance 所對應的類別中有定義的 attribute 進行存取。
在之前,我們還得在實作程式碼(.m檔)中以 @synthesize 將 attribute 與 property name 進行配對,這樣 compiler 才會在編譯階段把 getter/setter 加入至實作程式碼中。以現今的時間點,相信 synthesize 快被大家遺忘了…。
二、存取 Property 語法:用點號
「點號語法 (dot syntax)」是另一項專屬 property 的語法,背後只是代為呼叫該 property 對應的 getter/setter methods。可以不自己寫 getter/setter methods 及可以使用點號語法,說實在,真的是一個很省時間、很簡潔程式碼的作法。 Java 為何不引入這種語法糖呢…?
三、 Property attributes
好,問題來了:
- 如果我們只需要唯讀的 property 怎麼辦?也就是說,只想提供 getter method,但是不提供 setter method,做得到嗎?要怎麼告知 compiler 這項需求?
- 我們可以對 setter method 做進一步的控制嗎?根據我們對 heap 內物件保存方式的認知,我們可以在 setter method 內對傳入參數做進一步加工,例如 retain 它,那使用自動生成 setter method 時,該怎麼告知 compiler 這項需求?
- 我們可以撰寫自訂名稱的 getter/setter methods 嗎?
以上答案的解答,只在於:宣告 property 時,給予合適的設定即可。這裡用「設定」代稱「Property attribute」是由於不想和 object attribute 混淆。
書中分四類,我分三類:
- 和執行緒安全有關:nonatomic 及 atomic。一般用 nonatomic。為什麼?效能問題,而且使用良好的 GCD 操作取代對 NSThread 的控制,就大半可以不用擔心執行緒問題。
- 和 getter / setter methods 的生成有關:readwrite (預設值)、readonly(只生成 getter method)、getter=
(getter method 的命名)、setter= (setter method 的命名)。 - 和記憶體管理「語義」有關:
- assign 和 weak :兩者作用類似,但 assign 用在數量型別、weak 用在物件。明顯和 stack /heap 的設計原理有關。
- strong:和過去 MRD 時代的 retain 為相同語義。
- copy:和 strong 不同的是,它不 retain 物件,而是複製出一份自己用的複本。
- unsafe_unretained:在下一回的讀書會中,會有更進一步的討論。
四、非脆弱的ABI (Application Binary Interface)
這裡強調非脆弱的 ABI 讓 Objective-C 能夠在「如果類別定義發生改變,儲存在類別物件的偏移量會被更新」,因此能解決執行期由新類別定義導致的不兼容問題。而克服直接讀取實例變數可能造成偏移量偏差的方法之一,就是使用 property 提供的 getter / setter methods。
主題07
這個主題延伸了主題06對於 Property 的討論。作者強烈地建議:
在內部直接存取實例變數,但是用 Property來設定實例變數。
為什麼呢要直接存取實例變數?
- 會比較快,因為不必透過 method dispatch 機制。
- 不會觸發 KVO 機制。作者是這麼說的,但是和我的實作不合。直接 assign 給實例變數還是會觸發 KVO 機制。更具體地說,使用 addObserver:forKeyPath:options:context 進行監控的實例變數,還是會觸發 observeValueForKeyPath:ofObject:change:context 方法。
為什麼呢要用 Property來設定實例變數?
- 可以遵循由 property attribute 所規範的記憶體管理語義。
- 偵錯容易,因為我們可以在 getter / setter methods 裡設定中斷點。
不過這個建議不是哪裡都可以用的。因為:
- 由於子類別可以 override 父類別的 getter / setter methods,所以若在 init method 中使用 getter / setter 則可能在子類別建立時發生意想不到的錯誤。
- 也有相反的情形:我們必須在 init method 中使用 setter method,因為該實例變數是繼承來的,不透過 setter method 根本存取不到。
- 若希望運用 lazy initialization 機制:除非真的用到,不然不產生實例,那麼就該使用 getter method 來進行存取。
聽起來這個建議的例子挺多的…,我覺得可以當成是重構時的思考項,在程式碼初生的階段,可能不用想這麼多。
主題14
大概物件導向語言都有個重要但是卻不常被提及的物件,那就是「類別物件」。我喜歡用 ActiveRecord 的設計語義來詮釋類別與物件:類別是專有名稱,是唯一的;物件是普通名稱,是可數的。
在有 namespace 或 package 結構的程式語言中,類別全名指的是包含該 namespace 或 package 的名稱。例如:在 Java 中,String 類別的全名為「java.lang.String」。而對於 Objective-C 來說則沒有 namespace / package 的部分。就像在 Java 世界不會有第二個 java.lang.String 類別,在 Objective-C 來說,也只有一個 NSString 類別。(正常的情況下)
當程式試圖利用一個類別、生成該類別的實體(物件)時,會先截入該類別至記憶體、生成類別物件,此物件為 singleton object,然後才是以該類別物件做為模子生成對應該類別的物件。
Objective-C 的每個物件的「第一個成員」,被規範為一個用來定義物件自己所屬類別為何的變數。這個變數為「is a (是)」指標。可以觀察一下最基礎的 Class 類別的定義:objc-class.h。
在 Objective-C 的 Foundation 框架利用 Class 類別定義中的 「is-a 」指標,在執行期能進行物件型別的檢查,使得 NSObject 物件都能使用所謂的「內省 (introspection)」機制。說穿了,就是在執行期進一步確認某物件的確實類別、實作了哪些方法的判斷機制。
所謂的內省機制,至少包括了兩項方法:isMemberOfClass 及 isKindOfClass。
這兩個方尤其常用在從 collection 物件中取得單一 element 時。為什麼?來看看 NSArray 及 NSMutableArray 中的方法定義:
由上述兩個方法得知,自 collections 物件中取出的 element 或要存入的 element 並沒有限定為特定類別的實體。和 Java 5 以後版本比較起來,少了「泛型」語法。在 Java 中,會這樣產生一個「只能由 String 實體為 element 的 List 物件」:
在有 namespace 或 package 結構的程式語言中,類別全名指的是包含該 namespace 或 package 的名稱。例如:在 Java 中,String 類別的全名為「java.lang.String」。而對於 Objective-C 來說則沒有 namespace / package 的部分。就像在 Java 世界不會有第二個 java.lang.String 類別,在 Objective-C 來說,也只有一個 NSString 類別。(正常的情況下)
當程式試圖利用一個類別、生成該類別的實體(物件)時,會先截入該類別至記憶體、生成類別物件,此物件為 singleton object,然後才是以該類別物件做為模子生成對應該類別的物件。
Objective-C 的每個物件的「第一個成員」,被規範為一個用來定義物件自己所屬類別為何的變數。這個變數為「is a (是)」指標。可以觀察一下最基礎的 Class 類別的定義:objc-class.h。
在 Objective-C 的 Foundation 框架利用 Class 類別定義中的 「is-a 」指標,在執行期能進行物件型別的檢查,使得 NSObject 物件都能使用所謂的「內省 (introspection)」機制。說穿了,就是在執行期進一步確認某物件的確實類別、實作了哪些方法的判斷機制。
所謂的內省機制,至少包括了兩項方法:isMemberOfClass 及 isKindOfClass。
- isMemberOfClass:判斷某物件是否為特定類別的實體。
- isKindOfClass:判斷某物件是否為特定類別或其子類別的實體。
這兩個方尤其常用在從 collection 物件中取得單一 element 時。為什麼?來看看 NSArray 及 NSMutableArray 中的方法定義:
- - (id)objectAtIndex:(NSUInteger)index; //NSArray.h
- - (void)addObject:(id)anObject; //NSMutableArray
由上述兩個方法得知,自 collections 物件中取出的 element 或要存入的 element 並沒有限定為特定類別的實體。和 Java 5 以後版本比較起來,少了「泛型」語法。在 Java 中,會這樣產生一個「只能由 String 實體為 element 的 List 物件」:
而對於 NSArray 來說,它可以裝載的 element 並不限任何型態,可以說很自由,但是更容易造成混亂(至少提供了造成混亂的空間)。所以在從 NSArray 中取出 element 或設值/插入值時,會比較頻繁地使用 isMemberOfClass 或 isKindOfClass 這兩個內省機制中的方法。List<string> aList = new ArrayList<string>();
主題18
- 留意物件的可變性是否合理。
- 使用 collection 物件時,要留意裡面 element 的可變性。
以書中例子所言,若有個功能近似 DTO (Data Transfer Object) 的物件,此物件的 data source 是來自某 Web Service,那麼由於不會有「回傳改變後的值」的需求,所以該物件的可變性是不合理的。
這種情形下,作者建議使用 property attribute 對該物件加以控制。也就是說,把該物件納入 property 的一員,並使用 readonly 的 property attribute 對其加以約束即可。
此外,假若 property 為 collection 類別時,要留意:我們可以讓 collection 保有的 element 是固定的,但是那不表示 element 的內含值是不可變的。即便如此,也不表示我們必須針對每個 element 的存取以內省機制加以檢查。作者說:
…不要透過內省機制檢視回轉給你的任何物件而判斷它是否可變…你應該不惜代價避免那樣做(不應該使用這樣的內省技術)…
說到底, collection 中的 element 是否可變,端視加入的 element 本身的可變性。而是否加入具有可變性的 element 的判斷,應該由程式的使用方明確表達其意圖,而非由 API 的提供方預設立場地加以限制。更何況,還有其他的技術可以繞過這樣內省技術的檢查,終究是無法完整防禦的。
這個主題的範例最過癮的地方在於示範了類目延續類目 (class-continuation category)如何覆寫 @interface 區段的 property 宣告設定。
類目延續類目…這個名字有夠爛,我接下來只稱呼它:寫在實作檔中的「暱名 category」。
在 @interface 宣告區動段,我們可以宣告某 property 為 readonly,然後在實作檔中的暱名 category 區段再一次宣它它為 readwrite,這樣的話 Runtime Library 還是會替該 property 建立 setter method,只是除了類別本身實作檔外,其他外部類別的物件是無法讀寫該 property 的。
是嗎?不是…其實透過 KVO 機制,Runtime Library 還是可以幫你找到那個實際上有實作的 setter ,然後使用它。所以…只能說使用暱名 category 的方法很不錯,但是也不是完美的。
主題27
其實在上一個主題已經提及了這個主題的主角:類目延續類目 (class-continuation category)。好吧,再說一次:我接下來只稱呼它:寫在實作檔中的「暱名 category」。
簡單講,我們可以把不想寫在公開介面 (.h 檔)上的任何資訊寫在這個暱名 category 中。
這點對於一般程式開發來說,其實不算是非常必要的觀念,但是對於 API 開發者來說,盡可能透露出最少的訊息給外界使用者是極為重要的。因為曝露在公開介面的資訊是不可以隨便修改及移除的,任意地修改將為 API 既有使用者不必要的困擾。
所以不論是有不需要公開的:
- protocol
- IBOutlet
- IBAction
- method
- attribute
- property
都可以寫在暱名 category 中。事實上,以往在 Xcode 中開啟 Assistant Editor 時,會開啟對應 IB 上選取中 ViewController 的 .h 檔,而在 Xcode 5 版後就預設改成開啟 .m 檔,而非 .h,就是這個考量。
使用暱名 category 的另一個好處是可以比較好搭配 Objective-C++程式碼。這部分我完全不在乎,所以不在此討論。時間好少,有更重要的書要念、有更切實際的文章要寫。
沒有留言:
張貼留言