序言 打算分享一些有爭議的話題,並且表達一下我的看法。這是該系列的第一篇,我想討論的是:類的成員變數應該如何定義? 在 Objective-C 的語言的早期,類的私有成員變數是只能定義在 .h 的頭文件裡面的。像如下這樣: 之後,蘋果改進了 Objective-C,允許在 .m 裡面添加一個當前類的 ...
序言
打算分享一些有爭議的話題,並且表達一下我的看法。這是該系列的第一篇,我想討論的是:類的成員變數應該如何定義?
在 Objective-C 的語言的早期,類的私有成員變數是只能定義在 .h 的頭文件裡面的。像如下這樣:
@interface ViewController : UIViewController { @private NSInteger _value; }
之後,蘋果改進了 Objective-C,允許在 .m 裡面添加一個當前類的擴展,即沒有名字的 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
功能,就可以達到同樣的效果。
迴圈引用問題
直接用私有變數有個需要特別註意的地方,在 block 里直接寫 _property
相當於 self->_property
,雖然沒寫 self,但是暗含了對 self 的retain,容易造成迴圈引用。要記得用 weakSelf/strongSelf 大法,這一點確實是被很多人忽視的
寫在最後
其實我上面提到的這些問題都是小問題,影響不大。但是代碼風格的統一卻是大問題。所以不管你們項目中使用的是self.property
風格還是 _property
風格,問題都不大,但是如果你們同時使用這兩種風格,那麼就非常不好了。