解析型別引數

落雷發表於2023-10-04

原文在這裡

由 Ian Lance Taylor 釋出於2023年9月26日

slices 包函式簽名

slices.Clone 函式很簡單:它返回一個任意型別切片的副本:

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

這個方法有效的原因是:向容量為零的切片追加元素將分配一個新的底層陣列。函式體的長度最終比函式簽名的長度要短,函式體短是一方面原因,函式簽名長是另一方面原因。在本部落格文章中,我們將解釋為什麼函式簽名被寫成這樣。

Simple 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 方法,因此編譯器會報錯。

Flexible Clone

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

結果如下:

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

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

這裡我把約束寫為了?,但這只是一個佔位符。要使它工作,我們需要寫一個能讓我們編寫函式體的約束。對於Clone1,我們可以只使用any進行約束。但對於Clone2,這樣做不起作用:我們想要要求s是一個切片型別。

由於我們知道我們想要一個切片,切片的約束必須是一個切片。我們不關心切片元素型別是什麼,所以我們就像在Clone1中一樣將其命名為E

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)

編譯器告訴我們,我們不能將型別引數MySlice用於型別引數S,因為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 T2T1的底層型別是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的型別,然後使用另外兩個型別引數KV來拆解型別。

maps.Clone中,我們約束K必須是可比較的,因為這是對映鍵型別所要求的。我們可以按照自己的喜好約束元件型別。

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

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

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


孟斯特

宣告:本作品採用署名-非商業性使用-相同方式共享 4.0 國際 (CC BY-NC-SA 4.0)進行許可,使用時請註明出處。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 戀水無意


相關文章