gopl-zh.github.com/ch7/ch7-03.md
foreversmart 134adef31e ch7-03
2016-01-03 16:54:42 +08:00

8.0 KiB
Raw Blame History

7.3. 實現接口的條件

一個類型如果擁有一個接口需要的所有方法,那麽這個類型就實現了這個接口。例如,*os.File類型實現了io.ReaderWriterCloser和ReadWriter接口。*bytes.Buffer實現了ReaderWriter和ReadWriter這些接口但是它沒有實現Closer接口因爲它不具有Close方法。Go的程序員經常會簡要的把一個具體的類型描述成一個特定的接口類型。舉個例子*bytes.Buffer是io.Writer*os.Files是io.ReadWriter。

接口指定的規則非常簡單:表達一個類型屬於某個接口隻要這個類型實現這個接口。所以:

var w io.Writer
w = os.Stdout           // OK: *os.File has Write method
w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
w = time.Second         // compile error: time.Duration lacks Write method

var rwc io.ReadWriteCloser
rwc = os.Stdout         // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method

這個規則甚至適用於等式右邊本身也是一個接口類型

w = rwc                 // OK: io.ReadWriteCloser has Write method
rwc = w                 // compile error: io.Writer lacks Close method

因爲ReadWriter和ReadWriteCloser包含所有Writer的方法所以任何實現了ReadWriter和ReadWriteCloser的類型必定也實現了Writer接口

在進一步學習前必鬚先解釋表示一個類型持有一個方法當中的細節。迴想在6.2章中對於每一個命名過的具體類型T它一些方法的接收者是類型T本身然而另一些則是一個T的指針。還記得在T類型的參數上調用一個T的方法是合法的隻要這個參數是一個變量編譯器隱式的穫取了它的地址。但這僅僅是一個語法醣T類型的值不擁有所有*T指針的方法那這樣它就可能隻實現更少的接口。

舉個例子可能會更清晰一點。在第6.5章中IntSet類型的String方法的接收者是一個指針類型所以我們不能在一個不能尋址的IntSet值上調用這個方法

type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver

但是我們可以在一個IntSet值上調用這個方法

var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method

然而,由於隻有IntSet類型有String方法所有也隻有IntSet類型實現了fmt.Stringer接口

var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s  // compile error: IntSet lacks String method

12.8章包含了一個打印齣任意值的所有方法的程序然後可以使用godoc -analysis=type tool(§10.7.4)展示每個類型的方法和具體類型和接口之間的關繫

就像信封封裝和隱藏信件起來一樣,接口類型封裝和隱藏具體類型和它的值。卽使具體類型有其它的方法也隻有接口類型暴露齣來的方法會被調用到:

os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
os.Stdout.Close()                // OK: *os.File has Close method

var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer has Write method
w.Close()                // compile error: io.Writer lacks Close method

一個有更多方法的接口類型比如io.ReadWriter和少一些方法的接口類型,例如io.Reader進行對比更多方法的接口類型會告訴我們更多關於它的值持有的信息併且對實現它的類型要求更加嚴格。那麽關於interface{}類型,它沒有任何方法,請講齣哪些具體的類型實現了它?

這看上去好像沒有用但實際上interface{}被稱爲空接口類型是不可或缺的。因爲空接口類型對實現它的類型沒有要求,所以我們可以將任意一個值賦給空接口類型。

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

盡管不是很明顯從本書最早的的例子中我們就已經在使用空接口類型。它允許像fmt.Println或者5.7章中的errorf函數接受任何類型的參數。

對於創建的一個interface{}值持有一個booleanfloatstringmappointer或者任意其它的類型我們當然不能直接對它持有的值做操作因爲interface{}沒有任何方法。我們會在7.10章中學到一種用類型斷言來穫取interface{}中值的方法。

因爲接口實現隻依賴於判斷的兩個類型的方法,所以沒有必要定義一個具體類型和它實現的接口之間的關繫。也就是説,嚐試文檔化和斷言這種關繫幾乎沒有用,所以併沒有通過程序強製定義。下面的定義在編譯期斷言一個*bytes.Buffer的值實現了io.Writer接口類型:

// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)

因爲任意*bytes.Buffer的值甚至包括nil通過(*bytes.Buffer)(nil)進行顯示的轉換都實現了這個接口所以我們不必分配一個新的變量。併且因爲我們絶不會引用變量w我們可以使用空標識符來來進行代替。總的看這些變化可以讓我們得到一個更樸素的版本

// *bytes.Buffer must satisfy io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)

非空的接口類型比如io.Writer經常被指針類型實現尤其當一個或多個接口方法像Write方法那樣隱式的給接收者帶來變化的時候。一個結構體的指針是非常常見的承載方法的類型。

但是併不意味着隻有指針類型滿足接口類型甚至連一些有設置方法的接口類型也可能會被Go語言中其它的引用類型實現。我們已經看過slice類型的方法(geometry.Path, §6.1)和map類型的方法(url.Values, §6.2.1),後面還會看到函數類型的方法的例子(http.HandlerFunc, §7.7)。甚至基本的類型也可能會實現一些接口就如我們在7.4章中看到的time.Duration類型實現了fmt.Stringer接口。

一個具體的類型可能實現了很多不相關的接口。考慮在一個組織齣售數字文化産品比如音樂,電影和書籍的程序中可能定義了下列的具體類型:

Album
Book
Movie
Magazine
Podcast
TVEpisode
Track

我們可以把每個抽象的特點用接口來表示。一些特性對於所有的這些文化産品都是共通的,例如標題,創作日期和作者列表。

type Artifact interface {
    Title() string
    Creators() []string
    Created() time.Time
}

其它的一些特性隻對特定類型的文化産品纔有。和文字排版特性相關的隻有books和magazines還有隻有movies和TV劇集和屏幕分辨率相關。

type Text interface {
    Pages() int
    Words() int
    PageSize() int
}
type Audio interface {
    Stream() (io.ReadCloser, error)
    RunningTime() time.Duration
    Format() string // e.g., "MP3", "WAV"
}
type Video interface {
    Stream() (io.ReadCloser, error)
    RunningTime() time.Duration
    Format() string // e.g., "MP4", "WMV"
    Resolution() (x, y int)
}

這些接口不止是一種有用的方式來分組相關的具體類型和表示他們之間的共同特定。我們後面可能會發現其它的分組。舉例如果我們發現我們需要以同樣的方式處理Audio和Video我們可以定義一個Streamer接口來代表它們之間相同的部分而不必對已經存在的類型做改變。

type Streamer interface {
    Stream() (io.ReadCloser, error)
    RunningTime() time.Duration
    Format() string
}

每一個具體類型的組基於它們相同的行爲可以表示成一個接口類型。不像基於類的語言他們一個類實現的接口集合需要進行顯式的定義在Go語言中我們可以在需要的時候定義一個新的抽象或者特定特點的組而不需要脩改具體類型的定義。當具體的類型來自不同的作者時這種方式會特别有用。當然也確實沒有必要在具體的類型中指齣這些共性。