歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 為什麼我不喜歡Go語言式的接口(即Structural Typing)

為什麼我不喜歡Go語言式的接口(即Structural Typing)

日期:2017/3/1 9:31:48   编辑:Linux編程

所謂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>相差太多,它有的盡是些AddFirstInsertBefore方法等等。當然,LinkedList<T>List<T>都是ICollection<T>,所以我們可以放心地使用其中一小部分成員,它們的行為特征是明確的。

這方面的反面案例之一便是Java了。在Java類庫中,ArrayListLinkedList都實現了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分析程序集,檢查FromTo的泛型參數是否匹配,這樣也等於提供了編譯期的靜態檢查。此外,我還支持了協變逆變,還可以讓不需要返回值的接口方法兼容帶有返回值的方法(現在甚至還可以為其查找擴展方法),這可比簡單通過名稱和參數類型判斷要強大多了。

有了多種選擇,我才放心地說我喜歡哪個。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

Copyright © Linux教程網 All Rights Reserved