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 相关的文章。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值