所謂Go語言式的接口,就是不用顯示聲明類型T
實現了接口I
,只要類型T
的公開方法完全滿足接口I
的要求,就可以把類型T
的對象用在需要接口I
的地方。這種做法的學名叫做Structural Typing,有人也把它看作是一種靜態的Duck Typing。除了Go的接口以外,類似的東西也有比如Scala裡的Traits等等。有人覺得這個特性很好,但我個人並不喜歡這種做法,所以在這裡談談它的缺點。當然這跟動態語言靜態語言的討論類似,不能簡單粗暴的下一個“好”或“不好”的結論。
那麼就從頭談起:什麼是接口。其實通俗地講,接口就是一個協議,規定了一組成員,例如.NET裡的ICollection
接口:
interface ICollection { int Count { get; } object SyncRoot { get; } bool IsSynchronized { get; } void CopyTo(Array array, int index); }
這就是一個協議的全部了嗎?事實並非如此,其實接口還規定了每個行為的“特征”。打個比方,這個接口的Count
除了需要返回集合內元素的數目以外,還隱含了它需要在O(1)時間內返回這個要求。這樣一個使用了ICollection
接口的方法才能放心地使用Count
屬性來獲取集合大小,才能在知道這些特征的情況下選用正確的算法來編寫程序,而不用擔心帶來性能問題,這才能實現所謂的“面向接口編程”。當然這種“特征”並不單指“性能”上的,例如Count
還包含了“不修改集合內容”這種看似十分自然的隱藏要求,這都是ICollection
協議的一部分。
由此我們還可以解釋另外一些問題,例如為什麼.NET裡的List<T>
不叫做ArrayList<T>
(當然這些都只是我的推測)。我的想法是,由於List<T>
與IList<T>
接口是配套出現的,而像IList<T>
的某些方法,例如索引器要求能夠快速獲取元素,這樣使用IList<T>
接口的方法才能放心地使用下標進行訪問,而滿足這種特征的數據結構就基本與數組難以割捨了,於是名字裡的Array就顯得有些多余。
假如List<T>
改名為ArrayList<T>
,那麼似乎就暗示著IList<T>
可以有其他實現,難道是LinkedList<T>
嗎?事實上,LinkedList<T>
根本與IList<T>
沒有任何關系,因為它的特征和List<T>
相差太多,它有的盡是些AddFirst
、InsertBefore
方法等等。當然,LinkedList<T>
與List<T>
都是ICollection<T>
,所以我們可以放心地使用其中一小部分成員,它們的行為特征是明確的。
這方面的反面案例之一便是Java了。在Java類庫中,ArrayList
和LinkedList
都實現了List
接口,它們都有get
方法,傳入一個下標,返回那個位置的元素,但是這兩種實現中前者耗時O(1)後者耗時O(N),兩者大相近庭。那麼好,我現在要實現一個方法,它要求從第一個元素開始,返回每隔P個位置的元素,我們還能面向List
接口編程麼?假如我們依賴下標訪問,則外部一不小心傳入LinkedList
的時候,算法的時間復雜度就從期望的O(N/P)變成了O(N2/P)。假如我們選擇遍歷整個列表,則即便是ArrayList
我們也只能得到O(N)的效率。話說回來,Java類庫的List
接口就是個笑話,連Stack
類都實現了List
,真不知道當年的設計者是怎麼想的。
簡單地說,假如接口不能保證行為特征,則“面向接口編程”沒有意義。
而Go語言式的接口也有類似的問題,因為Structural Typing都只是從表面(成員名,參數數量和類型等等)去理解一個接口,並不關注接口的規則和含義,也沒法檢查。忘了是Coursera裡哪個課程中提到這麼一個例子:
interface IPainter { void Draw(); } interface ICowBoy { void Draw(); }
在英語中Draw同時具有“畫畫”和“拔槍”的含義,因此對於畫家(Painter)和牛仔(Cow Boy)都可以有Draw這個行為,但是兩者的含義截然不同。假如我們實現了一個“小明”類型,他明明只是一個畫家,但是我們卻讓他去跟其他牛仔決斗,這樣就等於讓他去送死嘛。另一方面,“小王”也可以既是一個“畫家”也是個“牛仔”,他兩種Draw都會,在C#裡面我們就可以把他實現為:
class XiaoWang : IPainter, ICowBoy { void IPainter.Draw() { // 畫畫 } void ICowBoy.Draw() { // 掏槍 } }
因此我也一直不理解Java的取捨標准。你說這樣一門強調面向對象強調接口強調設計的語言,還要求強制異常,怎麼就不支持接口的顯示實現呢?
這就是我更傾向於Java和C#中顯式標注異常的原因。因為程序是人寫的,完全不會因為一個類只是因為存在某些成員,就會被當做某些接口去使用,一切都是經過“設計”而不是自然發生的。就好像我們在泰國不會因為一個人看上去是美女就把它當做女人,這年頭的化妝和PS技術太可怕了。
我這裡再小人之心一把:我估計有人看到這裡會說我只是酸葡萄心理,因為C#中沒有這特性所以說它不好。還真不是這樣,早在當年我還沒聽說Structural Typing這學名的時候就考慮過這個問題。我寫了一個輔助方法,它可以將任意類型轉化為某種接口,例如:
XiaoMing xm = new XiaoMing(); ICowBoy cb = StructuralTyping.From(xm).To<ICowBoy>();
於是,我們就很快樂地將只懂畫畫的小明送去決斗了。其內部實現原理很簡單,只是使用Emit在運行時動態生成一個封裝類而已。此外,我還在編譯後使用Mono.Cecil分析程序集,檢查From
與To
的泛型參數是否匹配,這樣也等於提供了編譯期的靜態檢查。此外,我還支持了協變逆變,還可以讓不需要返回值的接口方法兼容帶有返回值的方法(現在甚至還可以為其查找擴展方法),這可比簡單通過名稱和參數類型判斷要強大多了。
有了多種選擇,我才放心地說我喜歡哪個。JavaScript中只能用回調編寫代碼,於是很多人說它是JavaScript的優點,說回調多麼多麼美妙我會深不以為然——只是沒法反抗開始享受罷了嘛……
這篇文章好像吐槽有點多?不過這小文章還挺爽的。
Linux系統入門學習-在Linux中安裝Go語言 http://www.linuxidc.com/Linux/2015-02/113159.htm
Ubuntu 安裝Go語言包 http://www.linuxidc.com/Linux/2013-05/85171.htm
《Go語言編程》高清完整版電子書 http://www.linuxidc.com/Linux/2013-05/84709.htm
Go語言並行之美 -- 超越 “Hello World” http://www.linuxidc.com/Linux/2013-05/83697.htm
我為什麼喜歡Go語言 http://www.linuxidc.com/Linux/2013-05/84060.htm
Go語言內存分配器的實現 http://www.linuxidc.com/Linux/2014-01/94766.htm