為什麼我不喜歡Go語言式的介面(即Structural Typing)
所謂Go語言式的介面,就是不用顯示宣告型別T
實現了介面I
,只要型別T
的公開方法完全滿足介面I
的要求,就可以把型別T
的物件用在需要介面I
的地方。這種做法的學名叫做Structural Typing,有人也把它看作是一種靜態的Duck Typing。除了Go的介面以外,類似的東西也有比如Scala裡的Traits等等。有人覺得這個特性很好,但我個人並不喜歡這種做法,所以在這裡談談它的缺點。當然這跟動態語言靜態語言的討論類似,不能簡單粗暴的下一個“好”或“不好”的結論。
那麼就從頭談起:什麼是介面。其實通俗的講,介面就是一個協議,規定了一組成員,例如.NET裡的ICollection
介面:
public 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的優點,說回撥多麼多麼美妙我會深不以為然——只是沒法反抗開始享受罷了嘛……
這篇文章好像吐槽有點多?不過這小文章還挺爽的。
(本文亦發表至個人部落格)
相關文章
- 評: 為什麼我不喜歡Go語言式的介面Go
- 為什麼我最喜歡的程式語言是 GoGo
- 為什麼我喜歡 Lisp 程式語言Lisp
- 我為什麼不喜歡框架框架
- 為什麼我喜歡富於表達性的程式語言
- 為什麼我喜歡JavaJava
- 為什麼我們不喜歡IT行業的7個原因行業
- 我為什麼會從程式不喜歡加{}到加{}
- 為什麼Go語言設計受到歡迎?Go
- 為什麼Python要比其他語言更受喜歡?Python
- 我為什麼喜歡程式設計程式設計
- 為什麼客戶不喜歡我們開發的軟體
- 為什麼我們越來越不喜歡用網站?網站
- 在 Go 語言中,我為什麼使用介面Go
- 我們為什麼要使用GO語言?Go
- 為什麼我喜歡JavaScript的Optional ChainingJavaScriptAI
- 日本玩家為什麼不喜歡PVP遊戲?遊戲
- [譯] 為什麼我更喜歡物件而不是switch語句物件
- 為什麼我們需要一門新語言——Go語言Go
- 程式猿為什麼不招妹子喜歡的原因
- 我們為什麼會喜歡挖礦遊戲?遊戲
- 為什麼我喜歡單獨程式設計程式設計
- 我不喜歡的行為之工作版
- 4.我為什麼喜歡用Dart中的字串?Dart字串
- 這麼多程式語言,為何Python深受喜歡?Python
- 程式老鳥:我為什麼喜歡敏捷開發框架敏捷框架
- 為什麼我們喜歡看別人在遊戲裡受苦遊戲
- 為什麼那麼多人要學習go語言?go語言有什麼特點?Go
- 為什麼很多公司都轉型go語言開發?Go語言能做什麼Go
- 為什麼開發者不喜歡市場人員的 8 個理由
- 為什麼我不推薦 JavsScript 為首選程式語言
- 為什麼我不推薦JavsScript為首選程式語言
- 為何我不喜歡使用儲存過程儲存過程
- 什麼是Go語言?Go語言有什麼特點?Go
- 開放出版:為什麼我們需要一門新語言?許式偉《Go語言程式設計》序Go程式設計
- 為什麼亞馬遜、臉書和Discord的開發人員喜歡Rust程式語言? - businessinsider亞馬遜RustIDE
- 為什麼 Go 語言能在中國這麼火?Go
- 你為什麼不應該過度關注go語言的逃逸分析Go