打算分享一些有爭議的話題,並且表達一下我的看法。這是該系列的第一篇,我想討論的是:類的成員變量應該如何定義?
在 Objective-C 的語言的早期,類的私有成員變量是只能定義在 .h 的頭文件裡面的。像如下這樣:
@interface ViewController : UIViewController { @private NSInteger _value; }
之後,蘋果改進了 Objective-C,允許在 .m 裡面添加一個特殊的匿名 Category,即沒有名字的 Category,來實現增加類的成員變量。像如下這樣:
@interface ViewController () @property (nonatomic) NSInteger value; @end
這樣的好處是,這些變量在頭文件中被徹底隱藏起來了,不用暴露給使用者。
接著,在 2013 年的 WWDC 中,蘋果進一步改進了 Objective-C,允許在 .m 的 @implementation 中直接添加類的私有成員變量。像如下這樣:
@implementation ViewController { NSInteger _value; }
於是,大家對於如何定義私有的成員變量上就產生的分歧。許多人喜歡用匿名的 Category 的方式來定義私有成員變量。但是,我個人更推薦在 @implementation 中直接添加類的私有成員變量。下面我做一些解釋。
歷史原因
首先早期的 iOS 程序員一定知道,在 2011 年 ARC 被推出之前,Objective-C 是需要手工地管理引用計數的。而對類的所有私有成員使用 self.property 的形式,就可以使編譯器為我們自動生成管理引用計數的代碼。在 2012 年前,這個 feature 還需要使用 @synthesize 關鍵字來啟用的。於是,蘋果通過在代碼規范中推薦和強調使用 self.property 的編程習慣,來讓大家避免在內存管理中遇到問題。而在 ARC 時代,這個編程習慣帶來的優勢不再存在了,因為編譯器會自動為我們管理引用計數,我們只需要關心不要造成循環引用問題就行了。
省心省事
剛剛說到,在類中完全使用 _property 的方式來訪問私有成員變量,是不會有內存管理上的問題的。但是使用 self.property 的方式來訪問私有變量是不是也是一樣不會有內存管理上的問題呢?確實也是,但是有一點需要注意:我們最好不要在 init 和 dealloc 中使用 self.property 的方式來訪問成員變量,這一點是寫在蘋果的官方文檔裡的,我在以前的文章裡也介紹過。(見:《不要在init和dealloc函數中使用accessor》)
所以,如果你用 self.property 來訪問私有成員變量。那麼你需要注意,在 init 和 dealloc 中不使用這種方式。這其實對程序員來說是一個負擔,你需要不停提醒自己有沒有犯錯。如果你使用完全的 _property 的方式來訪問私有成員變量,就不用想這一類問題了。
關於隱藏
大家知道,self.property 其實是調用了類的 [self property] 方法,所以這其實是有一層方法調用的隱藏,很多時候,我們需要延遲初使化一個類成員的時候,就會把這個成員的初使化方法寫在這個 [self property] 方法的實現中。
那麼問題來了,當你在閱讀別人代碼時,看到 self.property 的時候,你會想:這裡會不會有一些隱藏的函數實現?於是你需要跳轉到其方法實現中去查找。但是在實際開發中,大部分的 property 其實是使用編譯器自動生成的 Getter 和 Setter 方法,於是你會找不到實現,這個時候,你才知道:“哦,原來這段代碼並沒有做自定義的成員初使化工作”。
這種默認的隱藏在代碼中多了,會影響代碼的閱讀和維護。其實大部分的類成員變量都需要在類初使化方法中賦值,大部分的 UIViewController 的成員變量,都需要在 viewDidLoad 方法中賦值。那既然這樣,不如直接在相應的方法中用一個名為 setupProperty 方法直接進行初使化。這樣的好處是,代碼的可讀性更好了,self.property 只有需要延遲初使化的情況下才被使用。
關於這個還有一個小故事,我之前 Review 過一個同事的 iOS 端代碼,那個同事喜歡把 table view 的數據另外封裝成一個類,而我覺得這些數據其實就是一個數組,沒必要進行這一層封裝,最終我們爭論了比較久。我的觀點是,一切隱藏都是對代碼復雜性的增加,除非它帶來了好處,例如達到了代碼復用,提高了代碼的可維護性等,否則,沒有好處的封裝只會給代碼閱讀理解帶來成本。就我現在的經歷中,大部分的 table view 的數據都可以放在一個數組中,沒必要把這個數組封裝起來,另外提供一套操作這個數組數據的方法。
簡短的代碼更易讀
_property 的寫法比 self.property 更短,也更簡單。我認為這樣寫出來的代碼更方便閱讀。
執行速度更快,IPA體積更小
我之前從來沒想到過這兩者之間的速度和應用體積會有很大差別。不過一個同行(來自國外著名的社交網絡公司)告訴我,他們公司發現二者還是有不小的差距,如果你們的應用需要做一些深度優化,可以考慮一下把 self.property 換成 _property。但我覺得,大部分應用都應該是不需要做這種深度優化的。
KVO 和 KVC
是的,如果用 _property 這種寫法,就不能使用 KVO 和 KVC 了。但是我得反問一下,在一個類的內部,KVO 自己的私有成員變量算是一個好設計嗎?我們講類要”高內聚,低耦合”,KVO 是為了實現觀察者模式,讓對象之間相互解耦的。如果把 KVO 用在類的內部,KVO 自己的私有成員,我認為其實這不是一個很好的設計。
Computed Properties
在 Swift 中,引入了 Computed Properties 的概念,其實這在 Objective-C 中也有,只是沒有專門給它名字。如果一個 property 我們提供了對應的 setter 和 getter,並且沒有直接使用其對應的 _property 變量,那麼這個 property 就是所謂的 Computed Properties。
是的,在類的內部如果直接使用 _property 形式,也無法使用 Computed Properties 了,但我認為這影響不大。其實 Computed Properties 也就是一層對數據存取的封裝,我們另外實現兩個函數,分別對應數據的 setter 和 getter 功能,就可以達到同樣的效果。
寫在最後
其實我上面提到的這些問題都是小問題,影響不大。但是代碼風格的統一卻是大問題。所以不管你們項目中使用的是 self.property 風格還是 _property 風格,問題都不大,但是如果你們同時使用這兩種風格,那麼就非常不好了。
希望我的這篇文章能讓大家了解到在這方面的爭論,也希望大家能夠在這一點上,在公司內部達成統一。