The Go 部落格
解構類型參數
slices 套件函式簽章
slices.Clone
函式非常簡單:建立任何類型切片的副本。
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
這是可行的,因為附加到容量為零的切片會配置新的後援陣列。函式主體最後會比函式簽章短,部分原因是主體較短,但簽章很長也有關係。在本文中,我們將解釋為何簽章會以這種方式撰寫。
簡單的 Clone
我們將從撰寫簡單的通用 Clone
函式開始。這不是 slices
套件中的函式。我們想取得任何元素類型的切片,然後傳回新的切片。
func Clone1[E any](s []E) []E {
// body omitted
}
一般函數 Clone1
有單一類型參數 E
。它有一個單一參數 s
,其為 E
類型的切片,且回傳同一類型的切片。這個簽章對任何熟悉 Go 中泛型的人來說都很直觀。
不過,有一個問題。具名稱的切片類型在 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
為切片類型。
因為我們知道要切片,所以 S
的約束必須為切片。我們不關心切片元素類型為何,因此我們姑且稱之為 E
,就如同我們對 Clone1
所做的一樣。
func Clone3[S []E](s S) S // INVALID
這仍然是無效的,因為我們尚未宣告 E
。E
的類型引數可以是任何類型,這表示它本身也必須是類型參數。由於它可以是任何類型,因此它的約束為 any
。
func Clone4[S []E, E any](s S) S
這已經很接近了,而且至少可以編譯,但我們還沒有完全完成。如果我們編譯這個版本,當我們呼叫 Clone4(ms)
時,會出現錯誤。
MySlice does not satisfy []string (possibly missing ~ for []string in []string)
編譯器告訴我們,我們無法對類型參數 S
使用類型引數 MySlice
,因為 MySlice
不符合 []E
約束。這是因為 []E
做為約束只允許切片類型文字,例如 []string
。它不允許 MySlice
之類的名稱類型。
底層類型約束
就像錯誤訊息所提示的,答案是加上 ~
。
func Clone5[S ~[]E, E any](s S) S
再次說明,寫類型參數和約束 [S []E, E any]
表示,S
的類型參數可以是任何未命名切片類型,但不能是定義為切片文字的命名類型。寫 [S ~[]E, E any]
,加上 ~
,表示 S
的類型參數可以是任何其基礎類型為切片類型的類型。
對於任何命名類型 type T1 T2
,T1
的基礎類型就是 T2
的基礎類型。已預先宣告類型(例如 int
)或類型文字(例如 []string
)的基礎類型就是它本身。如需具體細節,請參閱語言規範。在我們的範例中,MySlice
的基礎類型就是 []string
。
由於 MySlice
的基礎類型是切片,我們可以將類型為 MySlice
的參數傳遞給 Clone5
。您可能已經注意到,Clone5
的簽章與 slices.Clone
的簽章相同。我們終於成功達到目標。
在繼續前進之前,我們來討論一下為什麼 Go 語法需要 ~
。看起來好像我們永遠都希望允許傳遞 MySlice
,那麼為什麼不將其設為預設值?或者,如果我們需要支援精確比對,為什麼不反過來,讓 []E
的約束允許命名類型,而 =[]E
的約束只允許切片類型文字呢?
要說明這一點,我們首先來看一下 [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
會傳遞一個切片給參數 s
。簡單的類型推論會讓編譯器推論型別參數 S
的類型引數是傳遞給 Clone
的切片類型。然後,類型推論功能強大到足以看出 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
參數的類型參數,並使用兩個其他類型參數 K
和 V
來解構類型。
在 maps.Clone
中,我們限制 K
為可比較類型的,就像映射鍵類型所需要的一樣。我們可以按照任何我們想要的方式限制元件類型。
func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)
這表示 WithStrings
的引數必須是基礎類型的切片類型,其中元素類型具有 String
方法。
由於所有 Go 型別都可以透過組成類型建立,因此我們始終可以使用類型參數來解構這些類型,並根據需要限制它們。
下一篇文章: 所有您一直想知道的類型推論的事項 - 以及更多內容
上一篇文章: 在 Go 1.22 中修正 for 迴圈
部落格索引