Go 部落格

解構型別引數

Ian Lance Taylor
2023 年 9 月 26 日

slices 包函式簽名

slices.Clone 函式很簡單:它會複製任意型別的 slice。

func Clone[S ~[]E, E any](s S) S {
    return append(s[:0:0], s...)
}

之所以能這樣工作,是因為向一個容量為零的 slice 新增元素會分配一個新的底層陣列。函式體比函式簽名短,一部分是因為函式體很短,但也因為簽名很長。在這篇部落格文章中,我們將解釋為什麼簽名會這樣寫。

簡單的 Clone

我們將從編寫一個簡單的泛型 Clone 函式開始。這不是 slices 包中的那個。我們想要接收任意元素型別的 slice,並返回一個新的 slice。

func Clone1[E any](s []E) []E {
    // body omitted
}

泛型函式 Clone1 有一個單獨的型別引數 E。它接收一個引數 s,其型別是 E 型別的 slice,並返回相同型別的 slice。對於熟悉 Go 中泛型的人來說,這個簽名很簡單。

然而,有一個問題。具名 slice 型別在 Go 中並不常見,但人們確實會使用它們。

// MySlice is a slice of strings with a special String method.
type MySlice []string

// String returns the printable version of a MySlice value.
func (s MySlice) String() string {
    return strings.Join(s, "+")
}

假設我們想複製一個 MySlice,然後獲取其可列印版本,但要將字串按排序順序排列。

func PrintSorted(ms MySlice) string {
    c := Clone1(ms)
    slices.Sort(c)
    return c.String() // FAILS TO COMPILE
}

不幸的是,這行不通。編譯器會報告一個錯誤:

c.String undefined (type []string has no field or method String)

如果我們手動例項化 Clone1,透過將型別引數替換為型別實參,我們可以看到問題所在。

func InstantiatedClone1(s []string) []string

Go 的 賦值規則 允許我們將 MySlice 型別的值傳遞給 []string 型別的引數,因此呼叫 Clone1 是沒問題的。但 Clone1 會返回一個 []string 型別的值,而不是 MySlice 型別的值。[]string 型別沒有 String 方法,因此編譯器會報告一個錯誤。

靈活的 Clone

為了解決這個問題,我們必須編寫一個 Clone 版本,它返回的型別與其引數的型別相同。如果我們能做到這一點,那麼當我們用 MySlice 型別的值呼叫 Clone 時,它將返回 MySlice 型別的結果。

我們知道它看起來應該像這樣。

func Clone2[S ?](s S) S // INVALID

這個 Clone2 函式返回一個型別與其引數相同的返回值。

這裡我將約束寫成了 ?,但這只是一個佔位符。為了讓它工作,我們需要編寫一個約束,允許我們編寫函式體。對於 Clone1,我們可以簡單地使用 any 作為元素型別的約束。對於 Clone2,這行不通:我們希望要求 s 是一個 slice 型別。

既然我們知道我們想要一個 slice,那麼 S 的約束必須是一個 slice。我們不關心 slice 的元素型別是什麼,所以我們把它叫做 E,就像我們在 Clone1 中做的那樣。

func Clone3[S []E](s S) S // INVALID

這仍然是無效的,因為我們還沒有宣告 EE 的型別實參可以是任何型別,這意味著它本身也必須是一個型別引數。由於它可以是任何型別,所以它的約束是 any

func Clone4[S []E, E any](s S) S

這已經接近完成了,至少可以編譯透過,但我們還沒有完全成功。如果我們編譯這個版本,在呼叫 Clone4(ms) 時會得到一個錯誤。

MySlice does not satisfy []string (possibly missing ~ for []string in []string)

編譯器告訴我們,不能將 MySlice 作為型別引數 S 的型別實參,因為 MySlice 不滿足 []E 的約束。這是因為 []E 作為約束只允許 slice 型別字面量,比如 []string。它不允許像 MySlice 這樣的具名型別。

底層型別約束

正如錯誤訊息所暗示的,答案是加上一個 ~

func Clone5[S ~[]E, E any](s S) S

重申一下,編寫型別引數和約束 [S []E, E any] 意味著 S 的型別實參可以是任何未命名的 slice 型別,但它不能是定義為 slice 字面量的具名型別。編寫 [S ~[]E, E any],加上 ~,意味著 S 的型別實參可以是任何底層型別是 slice 型別的型別。

對於任何具名型別 type T1 T2T1 的底層型別是 T2 的底層型別。像 int 這樣的預宣告型別或像 []string 這樣的型別字面量的底層型別就是型別本身。有關確切的細節,請參見語言規範。在我們的例子中,MySlice 的底層型別是 []string

由於 MySlice 的底層型別是 slice,我們可以將 MySlice 型別的值傳遞給 Clone5。您可能已經注意到,Clone5 的簽名與 slices.Clone 的簽名相同。我們終於達到了我們的目標。

在我們繼續之前,讓我們討論一下為什麼 Go 語法需要 ~。看起來我們總是想允許傳遞 MySlice,那麼為什麼不將其設為預設呢?或者,如果我們想支援精確匹配,為什麼不顛倒過來,這樣 []E 的約束就允許具名型別,而像 =[]E 這樣的約束只允許 slice 型別字面量呢?

為了解釋這一點,我們首先觀察到像 [T ~MySlice] 這樣的型別引數列表沒有意義。這是因為 MySlice 並不是任何其他型別的底層型別。例如,如果我們有一個定義,如 type MySlice2 MySlice,那麼 MySlice2 的底層型別是 []string,而不是 MySlice。因此,[T ~MySlice] 要麼不允許任何型別,要麼與 [T MySlice] 相同,只匹配 MySlice。無論哪種方式,[T ~MySlice] 都沒有用處。為了避免這種混淆,語言禁止 [T ~MySlice],編譯器會產生一個類似的錯誤:

invalid use of ~ (underlying type of MySlice is []string)

如果 Go 不要求波浪號,這樣 [S []E] 就會匹配任何底層型別為 []E 的型別,那麼我們就必須定義 [S MySlice] 的含義。

我們可以禁止 [S MySlice],或者說 [S MySlice] 只匹配 MySlice,但這兩種方法在處理預宣告型別時都會遇到麻煩。像 int 這樣的預宣告型別,其底層型別就是它本身。我們希望允許人們編寫接受任何底層型別為 int 的型別實參的約束。在今天的語言中,他們可以透過編寫 [T ~int] 來做到這一點。如果我們不要求波浪號,我們仍然需要一種方法來說“任何底層型別為 int 的型別”。自然的方法是 [T int]。這意味著 [T MySlice][T int] 的行為會不同,儘管它們看起來非常相似。

我們或許可以這樣說:[S MySlice] 匹配任何底層型別為 MySlice 的底層型別的型別,但這使得 [S MySlice] 變得不必要且令人困惑。

我們認為要求使用 ~ 並清楚地區分何時匹配底層型別而不是型別本身更好。

型別推斷

現在我們已經解釋了 slices.Clone 的簽名,讓我們看看型別推斷如何實際簡化 slices.Clone 的使用。記住,Clone 的簽名是:

func Clone[S ~[]E, E any](s S) S

呼叫 slices.Clone 時,會將一個 slice 傳遞給引數 s。簡單的型別推斷將允許編譯器推斷出型別引數 S 的型別實參就是傳遞給 Clone 的 slice 的型別。然後,型別推斷足夠強大,可以知道 E 的型別實參是傳遞給 S 的型別實參的元素型別。

這意味著我們可以寫:

    c := Clone(ms)

而無需寫:

    c := Clone[MySlice, string](ms)

如果我們不呼叫 Clone 而是引用它,我們確實需要為 S 指定型別實參,因為編譯器沒有什麼可以用來推斷它的。幸運的是,在這種情況下,型別推斷能夠從 S 的引數推斷出 E 的型別實參,而我們不必單獨指定它。

也就是說,我們可以寫:

    myClone := Clone[MySlice]

而無需寫:

    myClone := Clone[MySlice, string]

解構型別引數

我們在這裡使用的通用技術,即透過另一個型別引數 E 來定義一個型別引數 S,是一種在泛型函式簽名中解構型別的方法。透過解構型別,我們可以命名並約束型別的各個方面。

例如,這是 maps.Clone 的簽名。

func Clone[M ~map[K]V, K comparable, V any](m M) M

slices.Clone 類似,我們為引數 m 的型別使用一個型別引數,然後使用另外兩個型別引數 KV 來解構型別。

maps.Clone 中,我們將 K 約束為可比較的,這對於 map 的鍵型別是必需的。我們可以根據需要約束元件型別。

func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)

這表示 WithStrings 的引數必須是一個 slice 型別,並且其元素型別具有 String 方法。

由於所有 Go 型別都可以由元件型別構建而來,因此我們始終可以使用型別引數來解構這些型別並根據需要進行約束。

下一篇文章: 你一直想知道的關於型別推斷的一切 - 以及更多
上一篇文章: 修復 Go 1.22 中的 for 迴圈
部落格索引