原文: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 相关的文章。
有疑问加站长微信联系(非本文作者)