golang 與 duck typing

原文: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 相關的文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章