本文從OC的可變數據和不可變數據作為引子,開始聊聊我眼中的OC和SmallTalk,想到哪兒就聊到哪兒了。本文中的觀點都是個人觀點,如果大家理解不一樣,純屬正常,歡迎討論。關於可變數據和不可變數據就不多聊了,有很多文章已經聊過了。
OC中的可變/不可變數據
寫過OC都知道,OC的基礎類型分為Mutable和IMMutable兩種類型,分別對應可變數據和不可變數據。OC在具體實現的時候,使用了類簇這樣的設計模式。
類簇
不可變數據和可變數據內部實際上是一個家族類,多個類各司其職。而暴露給使用者的只有不可變數據和可變數據這兩類。並且可變數據繼承自不可變數據。這樣強調了OC的簡潔性,使調用者不需了解多個內部的家族類。
NSArray和 NSMutableArray 就是這樣的典型實現, 具體點我 。但是這樣也就限制住了拓展性。比如說,我現在想要實現一個數字排序的數組類,我要怎麽做?很簡單,創建一個繼承自 NSArry 的 NSOrderArray ,寫一些排序功能的代碼,初始化後自動給數組排序,查找的時候用二分查找就好了。迄今為止沒有問題,現在我想再實現一個可變類型的 NSMutableOrderArray ,我思考了一會,懵逼了。 NSMutableOrderArray 是該繼承自 NSMutableArray 還是 NSOrderArray ?如果繼承自 NSMutableArray ,那我就要重新寫排序代碼,如果繼承自 NSOrderArray ,那我就要重新寫可變代碼。這兩條路,雖然最後可以實現出來 NSOrderArray 和 NSMutableOrderArray ,但是繼承關系肯定是亂的。
如果我知道 NSArry 、 NSMutableArray 內部的家族類的細節, _NSArrayI 和 _NSArrayM ,就可以自由組合出我想要的數字排序數組類。很可惜,作為調用者這些細節被屏蔽掉了。
那我該如何實現出來這樣的類?像 NSOrderSet 一樣,將 NSOrderArray 繼承自 NSObject ,從頭再實現一個數組,然後再將 NSMutableOrderArray 繼承自 NSOrderArray 。這個工作量就太大了,還不如每次使用 NSArray 的時候再排序,犧牲點性能沒什麽關系。在這個過程中說明了,類簇是OC簡潔性和拓展性的權衡了。
如果不同類簇設計,有沒有別的設計方法解決這個問題,多繼承?C++將多繼承加入又拿掉,加入又拿掉。實際上多繼承帶來的麻煩和解決的問題,還不一定誰多,多繼承Pass。AOP?實現出面向切面編程的協議,比如” NSIMMutableSequence “、” NSMutableSequence “、” NSOrderSequence “,這三個協議分別對應不可變數據集合、可變數據集合、排序集合協議。實現的類組合想要的協議即可。這樣使簡潔性下降了,自己在實現 NSMutableOrderArray 的時候,conforms了” NSMutableSequence “和 “ NSOrderSequence “後必定要寫幾個關於可變集合的方法。不過,還可以接受,降低了點簡潔性換來了很強的拓展性。我個人覺得是個不錯的設計方式,Swift就更加向這種思想靠攏。
我們還可以從Apple的角度想想OC。你能想象Apple挑了一門復雜的語言,然後你要做一個APP,需要先學1年這個復雜的語言?Excuse me?這樣,Apple早就倒了。
橋接
既然同種數據類型有可變數據和不可變數據之分,這兩者就要提供互相轉換的方法。而OC中的拷貝就是這兩者的橋接。從不可變數據 mutableCopy 就可以得到可變數據,可變數據 copy 就可以得到不可變數據。
更多關於拷貝的問題, 這篇文章講的很好 ,推薦一下。 copy 實際上是對可變數據才有用的操作,對於不可變數據再復制出來一份一模一樣的數據是沒有意義的。對於可變數據 copy 就是,將狀態消除,制作出一份不可變的“快照”,而 mutableCopy 是制作出一份一模一樣的可變數據,就像是這個數據開啟了另一條時間線,另一個宇宙。OC中的設計就是這樣的:
@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration> - (id)copy; //淺復制 - (id)mutableCopy; //深復制出NSMutableArray @end @interface NSMutableArray<ObjectType> : NSArray<ObjectType> - (id)copy; //深復制出NSArry(快照) - (id)mutableCopy; //深復制出NSMutableArray(另一條時間線) @end
如果光看這兩個接口,我可能會這樣設計:
@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration> - (id)bridgeToMutable; //深復制出NSMutableArray @end @interface NSMutableArray<ObjectType> : NSArray<ObjectType> - (id)bridgeToIMMutable; //深復制出NSArry(快照) - (id)copy; //深復制出NSMutableArray(另一條時間線) @end
Apple將 copy 和 mutableCopy 放在了 NSObject 裏面,讓所有子類都可以不再多添加方法也沒有大問題,其中也可能有些內存管理上的考慮。但是我個人覺得後者這麽設計,接口會更加明確,思想體現的更加直接。
架構設計
2016年,國外iOS圈開始不斷嘗試IMMutable Model Layer,這其中有 Facebook 、 Pinterest 。Facebook還開源了Remodel加強這一實踐,以後有時間會寫篇文章好好深聊這個東西,此處先留坑。這實際上,也是響應式編程思想的體現。國內就不知何時會搞起IMMutable Model Layer這樣的架構設計,也許就在這2017年吧。
不同語言中的可變/不可變數據
python中的可變數據和不可變數據是根據數據類型定的,list和dict都是可變的,str和tuple都是不可變的。可變數據沒有不可變的版本,不可變數據也沒有可變的版本。
JS中的list、 map都是可變的,原本是沒有不可變版本的。Facebook開源了個 immutable.js ,提供這些數據的不可變版本。這樣JS中就和OC一樣數據可以分為可變數據和不可變數據了。
既然別的語言都不原生提供可變數據和不可變數據版本,那OC為什麽要搞特殊提供呢?使用過OC的,都聽說過OC的設計思想是基於SmallTalk的。要想了解為啥OC這麽設計,那就得追溯到SmallTalk。
SmallTalk
原句在 這裏 ,這是1993年給出的一份SmallTalk演變歷史。關於Local State在這一段:
This is probably a good place to comment on the difference between what we thought of as OOP-style and the superficial encapsulation called “abstract data types” that was just starting to be investigated in academic circles. Our early “LISP-pair” definition is an example of an abstract data type because it preserves the “field access” and “field rebinding” that is the hallmark of a data structure. Considerable work in the 60s was concerned with generalizing such structures [DSP ]. The “official” computer science world started to regard Simula as a possible vehicle for defining abstract data types (even by one of its inventors [Dahl 1970]), and it formed much of the later backbone of ADA. This led to the ubiquitous stack data-type example in hundreds of papers. To put it mildly, we were quite amazed at this, since to us, what Simula had whispered was something much stronger than simply reimplementing a weak and ad hoc idea. What I got from Simula was that you could now replace bindings and assignment with goals . The last thing you wanted any programmer to do is mess with internal state even if presented figuratively. Instead, the objects should be presented as sites of higher level behaviors more appropriate for use as dynamic components*.
文中主要說的是SmallTalk從LISP-Pair中主要學習的地方就是重新綁定和賦值(重新綁定和賦值實際上就是不可變數據,SICP中有提),並且不希望程序員亂用狀態。使用面向對象編程應呈現為,將高層次的抽象行為動態的綁定到對象身上。
還有下面這一段:
Where does the special efficiency of object-oriented design come from? This is a good question given that it can be viewed as a slightly different way to apply procedures to data-structures. Part of the effect comes from a much clearer way to represent a complex system. Here, the constraints are as useful as the generalities. Four techniques used together—persistent state, polymorphism, instantiation, and methods-as-goals for the object—account for much of the power. None of these require an “object-oriented language” to be employed—ALGOL 68 can almost be turned to this style—an OOPL merely focuses the designer’s mind in a particular fruitful direction. However, doing encapsulation right is a commitment not just to abstraction of state, but to eliminate state oriented metaphors from programming.
尤其是最後一句話,SmallTalk提出的面向對象編程思想,不僅要抽象出狀態,還要盡力幹掉狀態。這就明朗了,SmallTalk主張的是多使用不可變數據幹掉狀態。所以,OC要分為可變和不可變類型。能用不可變類型就用不可變類型,必須牽扯到狀態時,再用可變數據。其實,這也就看出來了這兩年提出的響應式編程、Facebook提出的immutable model layer都是SmallTalk早在幾十年前主張的東西,只不過我們沒有註重這種思想編程。然後在合適的時機,被人挖出來,重新強調一下。
既然聊到這裏,就接著來聊一聊SmallTalk吧,SmallTalk只有兩個核心思想:
- 一切皆對象(Object),包括3、4這樣的整形數字,包括bool類型。如果消息有參數,我想你已經猜到了,跟在:後面。
- 過程抽象即發消息,其中包括3+4這樣的簡單算術,可被解釋為給3發送一個4作為參數的+消息。
關於這點還有更醍醐灌頂的,甚至包括條件語句(if)和循環(for)都是向對象發消息。if語句的一般形式是這樣的
bool表達式 ifTrue:[ ] ifFalse:[ ]
這解釋為向bool表達式的結果true或者false對象,發送ifTrue:ifFalse:消息。如果是true就執行ifTrue的參數,如果false就執行ifFalse的參數。而[ ]表示的參數就是我們熟悉的塊!SmallTalk中的塊用[ ]表示,並且也是一個對象。
這一切都基於SmallTalk的思想:過程抽象即發消息。並且會呈現出在寫SmallTalk語言的時候,都是這樣的形式:<消息接受者><消息>。那SmallTalk如何解釋?這些消息是有結合優先級的,以下為優先級從高到低:
- 一元消息:沒有參數的字母消息,比如無參數的方法。
- 二元消息:非字母的消息,比如+。
- 關鍵字消息:有參數的字母消息,比如有參數的方法。
比如,年底你拿到了年終獎,又遇到了個心儀的女孩,然後你早上從床上醒來,發現這只是一場夢:
yourDream: yourAccount annualBonus + girl
這句結合的優先級是這樣:
yourDream: ((yourAccount annualBonus) + girl)
解釋結合的時候,還有個有趣的一點。基本算術加減乘除,都是消息,而且都是二元消息,優先級是一樣的。如果這樣寫,不會得到你想要的結果:
3 * 4 + 5 * 6 = 102
要想要得到你想要的結果,必須要手動加括號:
(3 * 4) + (5 * 6) = 42
如果將OC與SmallTalk做下對比,你會發現,OC可不是一切皆對象,也不是一切皆發消息。當然,OC也有與SmallTalk一致的地方:
- self不管在什麽上下文中,永遠代表消息的接受者。super代表將覆蓋的父類方法發送給消息的接受者。
- 只有對象可以訪問自己的變量,即變量私有。
- 子類可以覆蓋父類方法,但不能重新定義變量。
到這裏,再重溫一下這句話:使用面向對象編程應呈現為,將高層次的抽象行為動態的綁定到對象身上。SmallTalk整個語言呈現出<消息接受者><消息>,不就是這句話最好的證明嗎?
是不是很驚訝SmallTalk的純粹?不過,SmallTalk並不是工業級語言,這也代表著SmallTalk沒用那麽多的妥協和trade-off,是門理想語言。SmallTalk作為提出面向對象編程概念的語言,更多的是為後來面向對象編程工業級語言鋪好了路。
Tags: 文章 拓展
文章來源: