原文:http://floss.zoomquiet.io/data/20120904000006/index.html
追加:
http://blog.zhaojie.me/2013/04/why-i-dont-like-go-style-interface-or-structural-typing.html
從老趙的博文裏學到更精確的說法“Structural Typing”,屬於吐槽文,go粉慎入
什麼是 duck typing?
在面向對象的編程語言中,當某個地方(比如某個函數的參數)需要符合某個條件的變量(比如要求這個變量實現了某種方法)時,什麼是判斷這個變量是否“符合條件”的標準?
如果某種語言在這種情況下的標準是: 這個變量的類型是否實現了這個要求的方法(並不要求顯式地聲明),那麼這種語言的類型系統就可以稱爲 duck typing
Duck Typing
聽起來有點不好理解,舉例更爲直觀。看下面一段簡單的 Python 代碼:
1 def greeting(a):
2 return a.sayHello()
3
4 class Duck(object):
5 def sayHello(self):
6 print('ga ga ga!')
7
8 class Person(object):
9 def sayHello(self):
10 print('Hello!')
11
12 class Unknown(object):
13 pass
14
15 duck = Duck()
16 person = Person()
17 u = Unknown()
18 u.sayHello = duck.sayHello
19
20 greeting(duck)
21 greeting(person)
22 greeting(u) # 最後的輸出爲 'ga ga ga! Hello! ga ga ga!'
從哪裏可以看出 Python 是 duck typing 呢?
上面這段 Python 代碼中, greeting 函數對參數 a 只有一個要求: a 必須實現 sayHello 這個方法。因爲 Duck 類和 Person 類都實現了 sayHello,那麼這兩個類型的實例,duck 和 person,都可以用作 greeting 的參數。甚至一個空白的類 Unknown 的對象 u, 只要我們給它加上一個 sayHello 的屬性(上面代碼中第18 行),它也能作爲 greeting 的參數。
與其它類型系統的區別
以 Java爲例, 一個類必須顯式地聲明:“我實現了這個接口。是這樣實現的。” 然後才能用在任何要求這個接口的地方。
如果你有一個第三方的 Java 庫,這個庫中的某個類沒有聲明它實現了某個你自定義的接口,那麼即使這個類中真的有那些相應的方法,你也不能把這個類的對象用在那些要求你自定義的那個接口的地方。但如果在某種 duck typing的語言中, 你就可以這樣做,因爲它不要求一個類顯式地聲明它實現了某個接口。
Duck typing 的準則是 “If you can do it, you can be used here”。Wikipeida 上的一個非常形象的解釋是:
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
Golang 的類型系統
一般來講,使用 duck typing 的編程語言往往被歸類到“動態類型語言”或者“解釋型語言”裏,比如 Python, Javascript, Ruby 等等;而其它的類型系統往往被歸到“靜態類型語言“中,比如 C/C++/Java。
動態類型的好處很多,使用過 Python 的人都知道寫代碼寫起來很快。但是缺陷也是顯而易見的:錯誤往往要在運行時才能被發現。比如上面的 greeting 函數,你可以傳遞任何一個變量作爲參數,但是要是這個變量沒有 sayHello 這個方法或者屬性,那麼程序運行時就會出錯。相反,靜態類型語言往往在編譯時就是發現這類錯誤:如果某個變量的類型沒有顯式聲明實現了某個方法/接口,那麼,這個變量就不能用在要求一個實現了這個接口的地方。
Go 的類型系統採取了折中的辦法:
- 靜態類型系統
- 一個類型不需要顯式地聲明它實現了某個接口
- 但僅當某個變量的類型實現了某個接口的方法,這個變量才能用在要求這個接口的地方。
聽起來很繞,看代碼:
package main
import (
"fmt"
)
type ISayHello interface {
SayHello()
}
type Person struct {}
func (person Person) SayHello() {
fmt.Printf("Hello!")
}
type Duck struct {}
func (duck Duck) SayHello() {
fmt.Printf("ga ga ga!")
}
func greeting(i ISayHello) {
i.SayHello()
}
func main () {
person := Person{}
duck := Duck{}
var i ISayHello
i = person
greeting(i)
i = duck
greeting(i)
}
// 最後輸出: Hello! ga ga ga
代碼的內容與之前的 Python 代碼基本相同:
- 兩種類型 Duck 和 Person 都實現了 sayHello 這一方法
- 函數 greeting 要求一個實現了 sayHello 方法的變量。這個變量與一般變量不同,稱爲“接口變量”。 如果某個變量 t 的類型 T 實現了某個接口 I 所要求的所有方法,那麼這個變量 t 就能被賦值給 I 的接口變量 i。調用 i 的方法,最終就是調用 t 的方法
爲什麼說這是一種折中的方法:
- 第一,類型 T 不需要顯式地聲明它實現了接口 I。只要類型 T 實現了所有接口 I 規定的函數,它就自動地實現了接口 I。 這樣就像動態語言一樣省了很多代碼,少了許多限制。
- 第二,在把 duck 或者 person 傳遞給 greeting 前,需要顯式或者隱式地把它們轉換爲接口 I 的接口變量 i。這樣就可以和其它靜態類型語言一樣,在編譯時檢查參數的合法性。
正是因爲“接口變量”這一類型的存在,Golang 實現了它獨特的 “易用” 與 “安全” 二者兼得的多態機制。“不需要聲明實現接口”,這樣就省去了很多代碼,我對 C++和Java都不熟,因此不知道 Java 的 Interface 和 C++的Template寫起來感覺如何,但是 C語言的 GObject 庫裏,要聲明一個類實現了某個接口,需要寫不少規定的代碼。同時,轉換爲 接口變量這一過程是在編譯時就完成的,因此,可以在編譯時就找出動態語言裏在運行時才能發現的代碼錯誤。
在 Golang 的 standard library中,這一特性被使用得淋漓盡致。比如,用 fmt.Fprintf 向一個 http 連接寫入 http 響應:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
Golang 的 fmt.Fprintf 函數的第一個參數的類型是一個 io.Writer 接口的接口變量。
type Writer interface {
Write(p []byte) (n int, err error)
}
而 net/http 中的 http.ResponseWriter 代表了一個 http 連接,它實現了 Write() 這個方法,因此,它自動實現了 Writer 這一接口。所以,我們在 http 的請求處理函數時,就可以直接用 Fprintf 來向一個 http.ResponseWriter 對象寫入響應。
總結
Golang 是一門有意思且非常實用的語言。這是我第一篇關於 Golang 的技術文章,我計劃每週在寫代碼之外,花時間至少寫一篇與 Golang 相關的文章。