**本文解釋了協變、逆變和不變性是什麼,以及對 Go 類型系統的影響。特別是解釋了為什麼在 slices 中不可能有可變性。**一個 Go 初學者經常問的問題是“為什麼我不能把 `[]int` 類型變數傳遞給函數 `func ([]interface{ })`”?在這篇文章中,我想探討這個問題及其對 Go 的影響。但是可變性(本文所描述)的概念在其他語言也是有用的。可變性描述了子類型關係應用在複合類型中使用時發生的情況。在這種情況下,“A是B的子類型”意味著,A類型的執行個體始終可以被用作需要B類型的情境。Go 沒有明確的子類型關係,最接近的是可賦值性,它主要決定類型是否可以互換使用。介面也許是最重要的使用情境:如果類型T(無論它是具體類型,還是本身是介面)實現介面I,然後T可以被看作是I的子類型。從這個意義上講, `*bytes.Buffer` 是 `io.ReadWriter` 的子類型,`io.ReadWriter` 是 `io.Reader` 的子類型。所有類型都是 `interface{}` 的子類型。理解可變性含義的最簡單方法是查看函數類型。假設我們有一個類型和一個子類型,例如 `*bytes.Buffer` 是 `io.Reader` 的子類型。可以定義這樣一個函數 `func() *bytes.Buffer`。我們也可以把這個函數用作 `func() io.Reader`,我們只是把傳回值重新定義為 `io.Reader`。但反方向的不成立的:我們不能把函數 `func() io.Reader` 用作函數 `func() *bytes.Buffer`,因為不是每個 `io.Reader` 都可以成為 `*bytes.Buffer`。因此,函數傳回值可以保持子類型關係的方向為:如果A是B的子類型,則函數 `func() A` 可以是函數 `func() B` 的子類型。這叫做協變。```gofunc F() io.Reader { return new(bytes.Buffer)}func G() *bytes.Buffer { return new(bytes.Buffer)}func Use(f func() io.Reader) { useReader(f())}func main() { Use(F) // Works Use(G) // Doesn't work right now; but *could* be made equivalent to... Use(func() io.Reader { return G() })}```另一方面,假設我們有函數 `func(*bytes.Buffer)`。現在我們不能把它當作函數 `func(io.Reader)`,你不能用 `io.Reader` 作為參數來調用它。但我們可以反方向調用。如果我們用 `*bytes.Buffer` 作為參數,可以用它調用 `func(io.Reader)`。因此,函數的參數顛倒了子類型關係:如果A是B的子類型,那麼 `func(B)`可以是 `func(A)` 的子類型。這叫做逆變。```gofunc F(r io.Reader) { useReader(r)}func G(r *bytes.Buffer) { useReader(r)}func Use(f func(*bytes.Buffer)) { b := new(bytes.Buffer) f(b)}func main() { Use(F) // Doesn't work right now; but *could* be made equivalent to... Use(func(r *bytes.Buffer) { F(r) }) Use(G) // Works}```因此,`func` 對於參數是逆變值的,對於傳回值是協變的。當然,我們可以將這兩種性質結合起來:如果A和C分別是B和D的子類型,我們可以使 `func(B) C` 成為 `func(A) D` 的子類型,可以這樣轉換:```go// *os.PathError implements errorfunc F(r io.Reader) *os.PathError { // ...}func Use(f func(*bytes.Buffer) error) { b := new(bytes.Buffer) err := f(b) useError(err)}func main() { Use(F) // Could be made to be equivalent to Use(func(r *bytes.Buffer) error { return F(r) })}```然而,`func(A) C` 和 `func(B) D` 是不相容的。一個也不能成為另一個的子類型。```gofunc F(r *bytes.Buffer) *os.PathError { // ...}func UseF(f func(io.Reader) error) { b := strings.NewReader("foobar") err := f(b) useError(err)}func G(r io.Reader) error { // ...}func UseG(f func(*bytes.Buffer) *os.PathErorr) { b := new(bytes.Buffer) err := f() usePathError(err)}func main() { UseF(F) // Can't work, because: UseF(func(r io.Reader) error { return F(r) // type-error: io.Reader is not *bytes.Buffer }) UseG(G) // Can't work, because: UseG(func(r *bytes.Buffer) *os.PathError { return G(r) // type-error: error is not *os.PathError })}```因此,在這種情況下,複合類型之間沒有關係。這叫做不變性。現在,我們可以回到我們的問題:為什麼不能將 `[]int` 作為 `[]interface{}` 來使用?這實際上是問:“為什麼 `slices` 類型是不變的”?提問者假設,因為 `int` 是 `interface{}` 的子類型,所以 `[]int` 也應該是 `[]interface{}` 的子類型。然而,我們現在可以看一個簡單的問題。`slices` 支援(除了別的之外)兩個基本操作,我們可以粗略地轉化成函數調用:```goas := make([]A, 10)a := as[0] // func Get(as []A, i int) Aas[1] = a // func Set(as []A, i int, a A)```這明顯出現了問題:類型A既作為參數出現,也作為傳回型別出現。因此,它既有協變又有逆變。因此,在調用函數時有一個相對明確的答案來解釋可變性如何工作,它只是對於 `slices` 沒有太多的意義。讀取 `slices` 需要協變,但寫入 `slices` 需要逆變。換句話說,如果你需要使 `[]int` 成為 `[]interface{}` 的子類,你需要解釋這段代碼是如何工作的:```gofunc G() { v := []int{1,2,3} F(v) fmt.Println(v)}func F(v []interface{}) { // string is a subtype of interface{}, so this should be valid v[0] = "Oops"}````channel` 提供了另一個有趣的視角。雙向 `channel` 類型具有與 `slices` 類型相同的問題:接收時需要協變,而發送時需要逆變。但你可以限制 `channel` 的方向,只允許發送或接收操作。所以 `chan A` 和 `chan B` 可以沒有關係,我們可以使 `<-chan A` 成為 `<-chan B` 的子類,或 `chan<-B` 成為 `chan<-A` 的子類。在這種意義上,唯讀類型至少在理論可以允許 `slices` 的可變性。`[]int` 仍然不是 `[]interface{}` 的子類型,我們可以使 `ro[] int` 成為 `ro []interface` 的子類型(借用proposal中的文法)。最後,我想強調的是,所有這些都只是理論上為 Go 類型系統添加可變性的問題。我認為這很難,但即使我們能解決這些問題,仍然會遇到一些實際問題。其中最緊迫的是子類型的記憶體結構不同:```govar ( // super pseudo-code to illustrate x *bytes.Buffer // unsafe.Pointer y io.ReadWriter // struct{ itable *itab; value unsafe.Pointer } // where itable has two entries z io.Reader // struct{ itable *itab; value unsafe.Pointer } // where itable has one entry)```因此,即使你認為所有介面都具有相同的記憶體模型,它們實際上沒有,因為方法表具有不同的假定布局。所以在這樣的代碼中```gofunc Do(f func() io.Reader) { r := f() r.Read(buf)}func F() io.Reader { return new(bytes.Buffer)}func G() io.ReadWriter { return new(bytes.Buffer)}func H() *bytes.Buffer { return new(bytes.Buffer)}func main() { // All of F, G, H should be subtypes of func() io.Reader Do(F) Do(G) Do(H)}```還需要在某個地方將H返回的 `io.ReadWriter` 介面封裝成 `io.Reader` 介面,並需要在某個地方將G的返回的 `*bytes.Buffer` 可轉換為正確的 `io.Reader` 介面。這對於函數來說,不是一個大問題:編譯器可以在 `main` 函數調用時產生合適的封裝。當代碼中使用這種形式的子類型時會有一定的效能開銷。然而,這對於 `slices` 來說是一個很重要的問題。對於 `slices` 我們有兩種處理方式。(a)將 `[]int` 轉換為 `[]interface{}` 進行傳遞,意味著一個分配並進行完整的拷貝。(b)延遲 `int` 與 `interface{}` 的轉換,直到需要進行訪問時在進行轉換。這意味著現在每個 `slices` 訪問都必須通過一個間接函數調用,以防萬一有人傳遞給我們一個子類型。這兩種選擇都不符合 Go 的設計目標。
via: https://blog.merovius.de/2018/06/03/why-doesnt-go-have-variance-in.html
作者:Axel Wagner 譯者:althen 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
254 次點擊