評: 爲何我不喜歡Go語言式的接口

<!-- 爲何我不喜歡Go語言式的接口(評) -->html

最近在Go語言的QQ羣裏看到關於圖靈社區有牛人老趙吐槽許式偉《Go語言編程》的各類爭論.java

我以前也看了老趙吐槽許式偉《Go語言編程》的文章, 當時想老趙若是能將許大書中不足部分補充完善了也是一個好事情. 所以, 對老趙的後續文章甚是期待.算法

誰知道看了老趙以後的兩篇吐槽Go語言的文章, 發現徹底不是那回事情, 吐槽內容誤差太遠. 原本沒想摻和進來, 可是看到QQ羣裏和圖靈社區有不少人甚至把老趙的文章看成真理同樣. 實在忍不住, 昨天註冊了賬號, 進來也說下個人觀點.編程

這是老趙的幾篇文章:api

本文在圖靈社區的網址:數組

補充說明:安全

由於當前這篇文章主要是針對老趙的不喜歡Go語言式的接口作 評論. 由於標題的緣由, 也形成了很大的爭議性(由於不少人說我理解的不少觀點和老趙的原文不相符).數據結構

後面我會對Go語言的一些特性一些簡單的介紹, 可是不會是如今這種方式.閉包


所謂Go語言式的接口,就是不用顯示聲明類型T實現了接口I,只要類型T的公開方法徹底知足接口I的要求,就能夠把類型T的對象用在須要接口I的地方。這種作法的學名叫作Structural Typing,有人也把它看做是一種靜態的Duck Typing。除了Go的接口之外,相似的東西也有好比Scala裏的Traits等等。有人以爲這個特性很好,但我我的並不喜歡這種作法,因此在這裏談談它的缺點。固然這跟動態語言靜態語言的討論相似,不能簡單粗暴的下一個「好」或「很差」的結論。oracle

原文觀點:<br>

  • Go的隱式接口其實就是靜態的Duck Typing. 不少語言(主要是動態語言)早就有.
  • 靜態類型和動態類型沒有絕對的好和很差.

個人觀點:<br>

  • Go的隱式接口Duck Typing確實不是新技術, 可是在主流靜態編程語言中支持Duck Typing應該是不多的(不清楚目前是否只有Go語言支持).
  • 靜態類型和動態類型雖然沒有絕對的好和很差, 可是每一個都是有本身的優點的, 沒有哪個能夠包辦一切. 而Go是試圖結合靜態類型和動態類型(interface)各自的優點.

那麼就從頭談起:什麼是接口。其實通俗的講,接口就是一個協議,規定了一組成員,例如.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協議的一部分。

原文觀點:<br>

  • 接口就是一個協議, 規定了一組成員.
  • 接口還規定了每一個行爲對應時間複雜度的"特徵".
  • 接口還規定了每一個行爲還包含是否會修改集合的隱藏要求.

個人觀點:<br>

  • 第一條: 沒什麼可解釋的, 應該是接口的通俗含義.
  • 第二條: 可是接口還包含時間複雜度的"特徵"就比較扯了. 請問這個特徵是由語言特性來約束(語言如何約束?), 還只是由接口的文檔做補充說明(這是語言的特性嗎)?
  • 第三條: 這個還算是吐槽到了點子上. Go的接口確實不支持C++相似的const修飾, 除了接口外的method也不支持(Go的const關鍵字是另外一個語義).

可是, C++中有了const就真的安全了嗎?

class Foo {
	private: mutable Mutex mutex_;
	
	public: void doSomething()const {
		MutexLocker locker(&mutex_);
		// const 已經被繞過了
	}
};

C++中方法const修飾惟一的用處就是增長各類編譯麻煩, 對使用者沒法做出任何承諾. 使用者更關心的是doSomething的要作什麼, 上面的方法其實和void doSomethingConst()要表達的是相似的意思.

無論是靜態庫仍是動態庫, 哪一個能從庫一級保證某個函數是不能幹什麼的? 若是C++的const關鍵字並不能 真正的保證const, 而相似的實現細節(也包括前面提到的和時間複雜度相關的性能特徵)必須有文檔來補充. 那文檔應該以什麼形式提供(代碼註釋?Word文檔?其餘格式文檔?)? 這些文檔真多能保證每一個都會有人看嗎? 文檔說到底還只是人直接的口頭約定, 若是文檔真的那麼好使(還有實現), 那麼彙編語言也能夠解決一切問題.

那在Go語言是如何解決const和性能問題?

首先, 對於C語言的函數參數傳值的語義, const是必然的結果. 可是, 若是參數太大要考慮性能的話, 就會考慮傳指針(仍是傳值的語義), 經過傳指針就不能保證const的語義了. 若是連使用的庫函數都不能相信, 那怎麼就能相信它對於的頭文件所提供的const信息呢?

由於, const和性能是相互矛盾的. Go語言中若是想絕對安全, 那就傳值. 若是想要性能(或者是返回反作用), 那就傳指針:

type Foo int

// 要性能
func (self *Foo)Get() int {
	return *self
}
// 要安全
func (self Foo)GetConst() int {
	return self
}

Go語言怎麼對待性能問題(還有單元測試問題)? 答案是集成go test測試工具. 在Go語言中測試代碼是pkg(包含package main)的一個組成部分. 不只是普通的pkg能夠go test, package main也能夠用go test進行測試.

咱們給前面的代碼加上單元測試和性能測試.

// foo_test.go

func TestGet(t *testing.T) {
	var foo Foo = 0
	if v := foo.Get(); v != 0 {
		t.Errorf("Bad Get. Need=%v, Got=%v", 0, v)
	}
}
func TestGetConst(t *testing.T) {
	var foo Foo = 0
	if v := foo.GetConst(); v != 0 {
		t.Errorf("Bad GetConst. Need=%v, Got=%v", 0, v)
	}
}

func BenchmarkGet(b *testing.B) {
	var foo Foo = 0
	for i := 0; i < b.N; i++ {
		_ = foo.Get()
	}
}
func BenchmarkGetConst(b *testing.B) {
	var foo Foo = 0
	for i := 0; i < b.N; i++ {
		_ = foo.GetConst()
	}
}

固然, 最終的測試結果仍是給人來看的. 若是實現者/使用者故意搞破壞, 再好的工具也是沒辦法的.

由此咱們還能夠解釋另一些問題,例如爲何.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>,因此咱們能夠放心地使用其中一小部分紅員,它們的行爲特徵是明確的。

原文觀點:<br>

  • 推測: 由於爲了和IList<T>接口配套出現的緣由, 纔沒有將List<T>命名爲ArrayList<T>.
  • 由於IList<T>(這個應該是筆誤, 我以爲做者是說List<T>)索引器要求可以快速獲取元素, 這樣使用IList<T>接口的方法才能放心地使用下標進行訪問(實現的算法複雜度特徵向接口方向傳遞了).
  • 不能將List<T>改成ArrayList<T>的另外一個緣由是LinkedList<T>. 由於List<T>LinkedList<T>的時間複雜度不同, 因此不能是一個接口(大概是一個算法複雜度一個接口的意思?).
  • LinkedList<T>List<T>都屬於ICollection<T>這個祖宗接口.

個人觀點:<br>

  • 第一條: 我不知道原做者是怎麼推測的. 接口的本意就是要和實現分離. 如今卻徹底綁定到一塊兒了, 那這樣還要接口作什麼(一個Xxx<T>對應一個IXxx<T>接口)?
  • 第二條: 由於運行時向接口傳遞了某個時間複雜度的實現, 就推導出接口的都符合某種時間複雜度, 邏輯上根本就不通!
  • 第三條: 和前兩個差很少的意思, 沒什麼可說的.
  • 第四條: 這個應該是Go非入侵接口的優勢. C++/Java就是由於接口的入侵性, 才致使了接口和實現沒法徹底分離. 由於, C++/Java大部分時間都在整理接口間/實現間的祖宗八代之間的關係了(重要的不是如何分類, 而是能作什麼). 能夠參考許式偉給的Java的例子(瞭解祖宗八代之間的關係真的很重要嗎): http://docs.oracle.com/javase/1.4.2/docs/api/overview-tree.html.

這方面的反面案例之一即是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,真不知道當年的設計者是怎麼想的。

簡單地說,假如接口不能保證行爲特徵,則「面向接口編程」沒有意義。

原文觀點:<br>

  • Java的ArrayListLinkedList都實現了List接口, 可是get方法的時間複雜度不一樣.
  • 假如接口不能保證行爲特徵,則「面向接口編程」沒有意義。

個人觀點:<br>

  • 第一條: 這實際上是原做者列的一個前提, 是爲了推出第二條的結論. 可是, 我以爲這裏的邏輯一樣是有問題的. 有這個例子只能說明接口有它的不足, 可是怎麼就證實了 則「面向接口編程」沒有意義?
  • 第二條: 我要反問一句, 爲何非要在這裏使用接口(難道是被C++/Java的面向對象洗腦了)? 接口有它合適的地方(面向邏輯層面), 也有它不合適的地方(面向底層算法層面). 在這裏爲何不直接使用ArrayListLinkedList?

而Go語言式的接口也有相似的問題,由於Structural Typing都只是從表面(成員名,參數數量和類型等等)去理解一個接口,並不關注接口的規則和含義,也無法檢查。忘了是Coursera裏哪一個課程中提到這麼一個例子:

nterface IPainter {
    void Draw();
}

nterface ICowBoy {
     void Draw();
}

在英語中Draw同時具備「畫畫」和「拔槍」的含義,所以對於畫家(Painter)和牛仔(Cow Boy)均可以有Draw這個行爲,可是二者的含義大相徑庭。假如咱們實現了一個「小明」類型,他明明只是一個畫家,可是咱們卻讓他去跟其餘牛仔決鬥,這樣就等於讓他去送死嘛。另外一方面,「小王」也能夠既是一個「畫家」也是個「牛仔」,他兩種Draw都會,在C#裏面咱們就能夠把他實現爲:

class XiaoWang : IPainter, ICowBoy {
    void IPainter.Draw() {
         // 畫畫
    }

    void ICowBoy.Draw() {
         // 掏槍
    }
}

所以我也一直不理解Java的取捨標準。你說這樣一門強調面向對象強調接口強調設計的語言,還要求強制異常,怎麼就不支持接口的顯示實現呢?

原文觀點:<br>

  • 不一樣實現的Draw含義不一樣, 所以接口最好也能支持不一樣的實現.
  • Java/Go之類的接口都沒有C#的接口強大.

個人觀點:<br>

  • 第一條: 不要由於本身有個錘子, 就把什麼東西都看成釘子! 你這個是C#的例子(我不懂C#), 可是請不要往Go語言上套! 以前是C++搞出了個函數重載(語義仍是類似的, 可是簽名不一樣), 沒想到C#還搞了個支持同一個單詞不一樣含義的特性.
  • 第二條: 只能說原做者真的不懂Go語言.

Go語言爲何不支持這些花哨的特性? 由於, 它們太複雜且沒多大用處, 寫出的代碼很差理解(若是原做者不提示, 誰能發現Darw的不一樣含義這個坑?). Go語言的哲學是: "Less is more!".

看看Go語言該怎麼作:

type Painter interface {
	Draw()
}
type CowBoyer interface {
	DrawTheGun()
}

type XiaoWang struct {
	// ...
}

func (self *XiaoWang)Draw() {
	// ...
}
func (self *XiaoWang)DrawTheGun() {
	// ...
}

XiaoWang須要關心的只是本身有哪些功能(method), 至於祖宗關係開始根本不用關心. 等到XiaoWang各類特性逐漸成熟穩定以後, 發現新來的XiaoMing也有相似的功能特徵, 這個時候纔會考慮如何用接口來描述XiaoWangXiaoMing共同特徵.

這就是我更傾向於Java和C#中顯式標註異常的緣由。由於程序是人寫的,徹底不會由於一個類只是由於存在某些成員,就會被當作某些接口去使用,一切都是通過「設計」而不是天然發生的。就好像咱們<del>在泰國</del>不會由於一我的看上去是美女就把它當作女人,這年頭的化妝和PS技術太可怕了。

原文觀點:<br>

  • 接口是通過「設計」而不是天然發生的.
  • 接口有不足, 由於在泰國不能根據美女這個接口來推斷這我的是女人這個類型.

個人觀點:<br>

  • Go的哲學是先構造具體對象, 而後再根據共性慢慢概括出接口, 一開始不用關心祖宗八代的關係.
  • 那請問女人是怎麼定義的, 難道這不是一個接口?

我這裏再小人之心一把:我估計有人看到這裏會說我只是酸葡萄心理,由於C#中沒有這特性因此說它很差。還真不是這樣,早在當年我還沒據說Structural Typing這學名的時候就考慮過這個問題。我寫了一個輔助方法,它能夠將任意類型轉化爲某種接口,例如:

XiaoMing xm = new XiaoMing();
ICowBoy cb = StructuralTyping.From(xm).To<ICowBoy>();

因而,咱們就很快樂地將只懂畫畫的小明送去決鬥了。其內部實現原理很簡單,只是使用Emit在運行時動態生成一個封裝類而已。此外,我還在編譯後使用Mono.Cecil分析程序集,檢查FromTo的泛型參數是否匹配,這樣也等於提供了編譯期的靜態檢查。此外,我還支持了協變逆變,還可讓不須要返回值的接口方法兼容存在返回值的方法,這可比簡單經過名稱和參數類型判斷要強大多了。

原文觀點:<br>

  • C#接口的這個特性很NB...

個人觀點:<br>

咱們看看Go是該怎麼寫(基於前面的Go代碼, 沒有Draw重載):

var xm interface{} = new(XiaoWang)
cb := xm.(Painter).(CowBoyer)

可是, 我以爲這樣寫真的很變態. Go語言是爲了解決實際的工程問題的, 不是要像C++那樣成爲各類NB技術的大雜燴.

我始終認同一個觀點: 任何語言均可以寫出垃圾代碼, 可是不能以這些垃圾代碼來證實原語言也垃圾.

有了多種選擇,我才放心地說我喜歡哪一個。JavaScript中只能用回調編寫代碼,因而不少人說它是JavaScript的優勢,說回調多麼多麼美妙我會深不覺得然——只是無法反抗開始享受罷了嘛……

這篇文章好像吐槽有點多?不過這小文章還挺爽的。

這段不是接口相關, 懶得整理/吐槽了.


最後我只想說一個例子, 從C語言時代就很流行的printf函數. 咱們看看Go語言中是什麼樣子(fmt.Fprintf):

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)

在Go語言中, fmt.Fprintf只關心怎麼識別各類a ...interface{}, 怎麼format這些參數, 至於怎麼寫, 寫到哪裏去那徹底是w io.Writer的事情.

這裏第一個參數的w io.Writer就是一個接口, 它不只能夠寫到File, 也能夠寫到net.Conn, 準確的說是能夠寫到任何實現了io.Writer接口的對象中.

由於, Go語言接口的非入侵性, 咱們能夠獨立實現本身的對象, 只要符合io.Writer接口就行, 而後就能夠和fmt.Fprintf配合工做.

後面的可變參數interface{}一樣是一個接口, 它代替了C語言的void*, 用於格式化輸出各類類型的值. (更準確的講, 除了基礎類型, 參數a必須是一個實現了Stringer接口的擴展類型).

接口是一個徹底正交的特性, 能夠將Fprintf從各類a ...interface{}, 以及各類w io.Writer徹底剝離出來. Go語言也是這樣, struct等基礎類型的內存佈局仍是和C語言中同樣, 只是加了個method(在Go1.1中, method value就是一個普通閉包函數), 接口以及goroutine都是在沒有破壞原有的類型語義基礎上正交擴展(而不是像C++那樣搞個構造函數, 之後又是析構函數的).

我到很想知道, 在C++/C#/Java之類的語言中, 是如何實現fmt.Fprintf的.


套用原做者的一句話做爲結束: Go語言雖然有缺點, 即便老趙是牛人, 可是這篇吐槽也着實通常!

相關文章
相關標籤/搜索