Go 泛型之泛型约束
Go 泛型之泛型約束
目錄-
Go 泛型之泛型約束
- 一、引入
- 二、最寬松的約束:any
- 三、支持比較操作的內(nèi)置約束:comparable
- 四、自定義約束
- 五、類型集合(type set)
- 六、簡(jiǎn)化版的約束形式
- 七、約束的類型推斷
- 八、小結(jié)
一、引入
雖然泛型是開(kāi)發(fā)人員表達(dá)“通用代碼”的一種重要方式,但這并不意味著所有泛型代碼對(duì)所有類型都適用。更多的時(shí)候,我們需要對(duì)泛型函數(shù)的類型參數(shù)以及泛型函數(shù)中的實(shí)現(xiàn)代碼設(shè)置限制。泛型函數(shù)調(diào)用者只能傳遞滿足限制條件的類型實(shí)參,泛型函數(shù)內(nèi)部也只能以類型參數(shù)允許的方式使用這些類型實(shí)參值。在 Go 泛型語(yǔ)法中,我們使用類型參數(shù)約束(type parameter constraint)(以下簡(jiǎn)稱約束)來(lái)表達(dá)這種限制條件。
約束之于類型參數(shù)就好比函數(shù)參數(shù)列表中的類型之于參數(shù):
函數(shù)普通參數(shù)在函數(shù)實(shí)現(xiàn)代碼中可以表現(xiàn)出來(lái)的性質(zhì)與可以參與的運(yùn)算由參數(shù)類型限制,而泛型函數(shù)的類型參數(shù)就由約束(constraint)來(lái)限制。
2018 年 8 月由伊恩·泰勒和羅伯特·格瑞史莫主寫(xiě)的 Go 泛型第一版設(shè)計(jì)方案中,Go 引入了 contract 關(guān)鍵字來(lái)定義泛型類型參數(shù)的約束。但經(jīng)過(guò)約兩年的 Go 社區(qū)公示和討論,在 2020 年 6 月末發(fā)布的泛型新設(shè)計(jì)方案中,Go 團(tuán)隊(duì)又放棄了新引入的 contract 關(guān)鍵字,轉(zhuǎn)而采用已有的 interface 類型來(lái)替代 contract 定義約束。這一改變得到了 Go 社區(qū)的大力支持。使用 interface 類型作為約束的定義方法能夠最大程度地復(fù)用已有語(yǔ)法,并抑制語(yǔ)言引入泛型后的復(fù)雜度。
但原有的 interface 語(yǔ)法尚不能滿足定義約束的要求。所以,在 Go 泛型版本中,interface 語(yǔ)法也得到了一些擴(kuò)展,也正是這些擴(kuò)展給那些剛剛?cè)腴T Go 泛型的 Go 開(kāi)發(fā)者帶來(lái)了一絲困惑,這也是約束被認(rèn)為是 Go 泛型的一個(gè)難點(diǎn)的原因。
下面我們來(lái)看一下 Go 類型參數(shù)的約束, Go 原生內(nèi)置的約束、如何定義自己的約束、新引入的類型集合概念等。我們先來(lái)看一下 Go 語(yǔ)言的內(nèi)置約束,從 Go 泛型中最寬松的約束:any 開(kāi)始。
二、最寬松的約束:any
無(wú)論是泛型函數(shù)還是泛型類型,其所有類型參數(shù)聲明中都必須顯式包含約束,即便你允許類型形參接受所有類型作為類型實(shí)參傳入也是一樣。那么我們?nèi)绾伪磉_(dá)“所有類型”這種約束呢?我們可以使用空接口類型(interface{})來(lái)作為類型參數(shù)的約束:
func Print[T interface{}](sl []T) {
// ... ...
}
func doSomething[T1 interface{}, T2 interface{}, T3 interface{}](t1 T1, t2 T2, t3 T3) {
// ... ...
}
不過(guò)使用 interface{} 作為約束至少有以下幾點(diǎn)“不足”:
- 如果存在多個(gè)這類約束時(shí),泛型函數(shù)聲明部分會(huì)顯得很冗長(zhǎng),比如上面示例中的
doSomething的聲明部分; -
interface{}包含{}這樣的符號(hào),會(huì)讓本已經(jīng)很復(fù)雜的類型參數(shù)聲明部分顯得更加復(fù)雜; - 和
comparable、Sortable、ordered這樣的約束命名相比,interface{}作為約束的表意不那么直接。
為此,Go 團(tuán)隊(duì)在 Go 1.18 泛型落地的同時(shí)又引入了一個(gè)預(yù)定義標(biāo)識(shí)符:any。any 本質(zhì)上是 interface{} 的一個(gè)類型別名:
// $GOROOT/src/builtin/buildin.go
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
這樣,我們?cè)诜盒皖愋蛥?shù)聲明中就可以使用 any 替代 interface{},而上述 interface{} 作為類型參數(shù)約束的幾點(diǎn)“不足”也隨之被消除掉了。
any 約束的類型參數(shù)意味著可以接受所有類型作為類型實(shí)參。在函數(shù)體內(nèi),使用 any 約束的形參 T 可以用來(lái)做如下操作:
- 聲明變量
- 同類型賦值
- 將變量傳給其他函數(shù)或從函數(shù)返回
- 取變量地址
- 轉(zhuǎn)換或賦值給
interface{}類型變量 - 用在類型斷言或 type switch 中
- 作為復(fù)合類型中的元素類型
- 傳遞給預(yù)定義的函數(shù),比如
new
下面是 any 約束的類型參數(shù)執(zhí)行這些操作的一個(gè)示例:
// any.go
func doSomething[T1, T2 any](t1 T1, t2 T2) T1 {
var a T1 // 聲明變量
var b T2
a, b = t1, t2 // 同類型賦值
_ = b
f := func(t T1) {
}
f(a) // 傳給其他函數(shù)
p := &a // 取變量地址
_ = p
var i interface{} = a // 轉(zhuǎn)換或賦值給interface{}類型變量
_ = i
c := new(T1) // 傳遞給預(yù)定義函數(shù)
_ = c
f(a) // 將變量傳給其他函數(shù)
sl := make([]T1, 0, 10) // 作為復(fù)合類型中的元素類型
_ = sl
j, ok := i.(T1) // 用在類型斷言中
_ = ok
_ = j
switch i.(type) { // 作為type switch中的case類型
case T1:
case T2:
}
return a // 從函數(shù)返回
}
但如果對(duì) any 約束的類型參數(shù)進(jìn)行了非上述允許的操作,比如相等性或不等性比較,那么 Go 編譯器就會(huì)報(bào)錯(cuò):
// any.go
func doSomething[T1, T2 any](t1 T1, t2 T2) T1 {
var a T1
if a == t1 { // 編譯器報(bào)錯(cuò):invalid operation: a == t1 (incomparable types in type set)
}
if a != t1 { // 編譯器報(bào)錯(cuò):invalid operation: a != t1 (incomparable types in type set)
}
... ...
}
所以說(shuō),如果我們想在泛型函數(shù)體內(nèi)部對(duì)類型參數(shù)聲明的變量實(shí)施相等性(==)或不等性比較(!=)操作,我們就需要更換約束,這就引出了 Go 內(nèi)置的另外一個(gè)預(yù)定義約束:comparable。
三、支持比較操作的內(nèi)置約束:comparable
Go 泛型提供了預(yù)定義的約束:comparable,其定義如下:
// $GOROOT/src/builtin/buildin.go
// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }
不過(guò)從上述這行源碼我們?nèi)匀粺o(wú)法直觀看到 comparable 的實(shí)現(xiàn)細(xì)節(jié),Go 編譯器會(huì)在編譯期間判斷某個(gè)類型是否實(shí)現(xiàn)了 comparable 接口。
根據(jù)其注釋說(shuō)明,所有可比較的類型都實(shí)現(xiàn)了 comparable 這個(gè)接口,包括:布爾類型、數(shù)值類型、字符串類型、指針類型、channel 類型、元素類型實(shí)現(xiàn)了 comparable 的數(shù)組和成員類型均實(shí)現(xiàn)了 comparable 接口的結(jié)構(gòu)體類型。下面的例子可以讓我們直觀地看到這一點(diǎn):
// comparable.go
type foo struct {
a int
s string
}
type bar struct {
a int
sl []string
}
func doSomething[T comparable](t T) T {
var a T
if a == t {
}
if a != t {
}
return a
}
func main() {
doSomething(true)
doSomething(3)
doSomething(3.14)
doSomething(3 + 4i)
doSomething("hello")
var p *int
doSomething(p)
doSomething(make(chan int))
doSomething([3]int{1, 2, 3})
doSomething(foo{})
doSomething(bar{}) // bar does not implement comparable
}
我們看到,最后一行 bar 結(jié)構(gòu)體類型因?yàn)閮?nèi)含不支持比較的切片類型,被 Go 編譯器認(rèn)為未實(shí)現(xiàn) comparable 接口,但除此之外的其他類型作為類型實(shí)參都滿足 comparable 約束的要求。
此外還要注意,comparable 雖然也是一個(gè) interface,但它不能像普通 interface 類型那樣來(lái)用,比如下面代碼會(huì)導(dǎo)致編譯器報(bào)錯(cuò):
var i comparable = 5 // 編譯器錯(cuò)誤:cannot use type comparable outside a type constraint: interface is (or embeds) comparable
從編譯器的錯(cuò)誤提示,我們看到:comparable 只能用作修飾類型參數(shù)的約束。
四、自定義約束
我們知道,Go 泛型最終決定使用 interface 語(yǔ)法來(lái)定義約束。這樣一來(lái),凡是接口類型均可作為類型參數(shù)的約束。下面是一個(gè)使用普通接口類型作為類型參數(shù)約束的示例:
// stringify.go
func Stringify[T fmt.Stringer](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
type MyString string
func (s MyString) String() string {
return string(s)
}
func main() {
sl := Stringify([]MyString{"I", "love", "golang"})
fmt.Println(sl) // 輸出:[I love golang]
}
這個(gè)例子中,我們使用的是 fmt.Stringer 接口作為約束。一方面,這要求類型參數(shù) T 的實(shí)參必須實(shí)現(xiàn) fmt.Stringer 接口的所有方法;另一方面,泛型函數(shù) Stringify 的實(shí)現(xiàn)代碼中,聲明的 T 類型實(shí)例(比如 v)也僅被允許調(diào)用 fmt.Stringer 的 String 方法。
這類基于行為(方法集合)定義的約束對(duì)于習(xí)慣了 Go 接口類型的開(kāi)發(fā)者來(lái)說(shuō),是相對(duì)好理解的。定義和使用起來(lái),與下面這樣的以接口類型作為形參的普通 Go 函數(shù)相比,區(qū)別似乎不大:
func Stringify(s []fmt.Stringer) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
但現(xiàn)在我想擴(kuò)展一下上面 stringify.go 這個(gè)示例,將 Stringify 的語(yǔ)義改為只處理非零值的元素:
// stringify_without_zero.go
func StringifyWithoutZero[T fmt.Stringer](s []T) (ret []string) {
var zero T
for _, v := range s {
if v == zero { // 編譯器報(bào)錯(cuò):invalid operation: v == zero (incomparable types in type set)
continue
}
ret = append(ret, v.String())
}
return ret
}
我們看到,針對(duì) v 的相等性判斷導(dǎo)致了編譯器報(bào)錯(cuò),我們需要為類型參數(shù)賦予更多的能力,比如支持相等性和不等性比較。這讓我們想起了我們剛剛學(xué)過(guò)的 Go 內(nèi)置約束 comparable,實(shí)現(xiàn) comparable 的類型,便可以支持相等性和不等性判斷操作了。
我們知道,comparable 雖然不能像普通接口類型那樣聲明變量,但它卻可以作為類型嵌入到其他接口類型中,下面我們就擴(kuò)展一下上面示例:
// stringify_new_without_zero.go
type Stringer interface {
comparable
String() string
}
func StringifyWithoutZero[T Stringer](s []T) (ret []string) {
var zero T
for _, v := range s {
if v == zero {
continue
}
ret = append(ret, v.String())
}
return ret
}
type MyString string
func (s MyString) String() string {
return string(s)
}
func main() {
sl := StringifyWithoutZero([]MyString{"I", "", "love", "", "golang"}) // 輸出:[I love golang]
fmt.Println(sl)
}
在這個(gè)示例里,我們自定義了一個(gè) Stringer 接口類型作為約束。在該類型中,我們不僅定義了 String 方法,還嵌入了 comparable,這樣在泛型函數(shù)中,我們用 Stringer 約束的類型參數(shù)就具備了進(jìn)行相等性和不等性比較的能力了!
但我們的示例演進(jìn)還沒(méi)有完,現(xiàn)在相等性和不等性比較已經(jīng)不能滿足我們需求了,我們還要為之加上對(duì)排序行為的支持,并基于排序能力實(shí)現(xiàn)下面的 StringifyLessThan 泛型函數(shù):
func StringifyLessThan[T Stringer](s []T, max T) (ret []string) {
var zero T
for _, v := range s {
if v == zero || v >= max {
continue
}
ret = append(ret, v.String())
}
return ret
}
但現(xiàn)在當(dāng)我們編譯上面 StringifyLessThan 函數(shù)時(shí),我們會(huì)得到編譯器的報(bào)錯(cuò)信息 invalid operation: v >= max (type parameter T is not comparable with >=)。Go 編譯器認(rèn)為 Stringer 約束的類型參數(shù) T 不具備排序比較能力。
如果連排序比較性都無(wú)法支持,這將大大限制我們泛型函數(shù)的表達(dá)能力。但是 Go 又不支持運(yùn)算符重載(operator overloading),不允許我們定義出下面這樣的接口類型作為類型參數(shù)的約束:
type Stringer[T any] interface {
String() string
comparable
>(t T) bool
>=(t T) bool
<(t T) bool
<=(t T) bool
}
那我們又該如何做呢?別擔(dān)心,Go 核心團(tuán)隊(duì)顯然也想到了這一點(diǎn),于是對(duì) Go 接口類型聲明語(yǔ)法做了擴(kuò)展,支持在接口類型中放入類型元素(type element)信息,比如下面的 ordered 接口類型:
type ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 | ~string
}
在這個(gè)接口類型的聲明中,我們沒(méi)有看到任何方法,取而代之的是一組由豎線 “|” 分隔的、帶著小尾巴 “~” 的類型列表。這個(gè)列表表示的是,以它們?yōu)榈讓宇愋停╱nderlying type)的類型都滿足 ordered 約束,都可以作為以 ordered 為約束的類型參數(shù)的類型實(shí)參,傳入泛型函數(shù)。
我們將其組合到我們聲明的 Stringer 接口中,然后應(yīng)用一下我們的 StringifyLessThan 函數(shù):
type Stringer interface {
ordered
comparable
String() string
}
func main() {
sl := StringifyLessThan([]MyString{"I", "", "love", "", "golang"}, MyString("cpp")) // 輸出:[I]
fmt.Println(sl)
}
這回編譯器沒(méi)有報(bào)錯(cuò),并且程序輸出了預(yù)期的結(jié)果。
好了,看了那么多例子,是時(shí)候正式對(duì) Go 接口類型語(yǔ)法的擴(kuò)展做一個(gè)說(shuō)明了。下面是擴(kuò)展后的接口類型定義的組成示意圖:
我們看到,新的接口類型依然可以嵌入其他接口類型,滿足組合的設(shè)計(jì)哲學(xué);除了嵌入的其他接口類型外,其余的組成元素被稱為接口元素(interface element)。
接口元素也有兩類,一類就是常規(guī)的方法元素(method element),每個(gè)方法元素對(duì)應(yīng)一個(gè)方法原型;另一類則是此次擴(kuò)展新增的類型元素(type element),即在接口類型中,我們可以放入一些類型信息,就像前面的 ordered 接口那樣。
類型元素可以是單個(gè)類型,也可以是一組由豎線 “|” 連接的類型,豎線 “|” 的含義是“并”,這樣的一組類型被稱為 union element。無(wú)論是單個(gè)類型,還是 union element 中由 “|” 分隔的類型,如果類型中不帶有 “~” 符號(hào)的類型就代表其自身;而帶有 “~” 符號(hào)的類型則代表以該類型為底層類型(underlying type)的所有類型,這類帶有 “~” 的類型也被稱為 approximation element,如下面示例:
type Ia interface {
int | string // 僅代表int和string
}
type Ib interface {
~int | ~string // 代表以int和string為底層類型的所有類型
}
下圖是類型元素的分解說(shuō)明,供你參考:
不過(guò)要注意的是:union element 中不能包含帶有方法元素的接口類型,也不能包含預(yù)定義的約束類型,如 comparable。
擴(kuò)展后,Go 將接口類型分成了兩類,一類是基本接口類型(basic interface type),即其自身和其嵌入的接口類型都只包含方法元素,而不包含類型元素。基本接口類型不僅可以當(dāng)做常規(guī)接口類型來(lái)用,即聲明接口類型變量、接口類型變量賦值等,還可以作為泛型類型參數(shù)的約束。
除此之外的非空接口類型都屬于非基本接口類型,即直接或間接(通過(guò)嵌入其他接口類型)包含了類型元素的接口類型。這類接口類型僅可以用作泛型類型參數(shù)的約束,或被嵌入到其他僅作為約束的接口類型中,下面的代碼就很直觀地展示了這兩種接口類型的特征:
type BasicInterface interface { // 基本接口類型
M1()
}
type NonBasicInterface interface { // 非基本接口類型
BasicInterface
~int | ~string // 包含類型元素
}
type MyString string
func (MyString) M1() {
}
func foo[T NonBasicInterface](a T) { // 非基本接口類型作為約束
}
func bar[T BasicInterface](a T) { // 基本接口類型作為約束
}
func main() {
var s = MyString("hello")
var bi BasicInterface = s // 基本接口類型支持常規(guī)用法
var nbi NonBasicInterface = s // 非基本接口不支持常規(guī)用法,導(dǎo)致編譯器錯(cuò)誤:cannot use type NonBasicInterface outside a type constraint: interface contains type constraints
bi.M1()
nbi.M1()
foo(s)
bar(s)
}
看到這里,你可能會(huì)覺(jué)得有問(wèn)題了:基本接口類型,由于其僅包含方法元素,我們依舊可以基于之前講過(guò)的方法集合,來(lái)確定一個(gè)類型是否實(shí)現(xiàn)了接口,以及是否可以作為類型實(shí)參傳遞給約束下的類型形參。但對(duì)于只能作為約束的非基本接口類型,既有方法元素,也有類型元素,我們?nèi)绾闻袛嘁粋€(gè)類型是否滿足約束,并作為類型實(shí)參傳給類型形參呢?
這時(shí)候我們就需要 Go 泛型落地時(shí)引入的新概念:類型集合(type set),類型集合將作為后續(xù)判斷類型是否滿足約束的基本手段。
五、類型集合(type set)
類型集合(type set)的概念是 Go 核心團(tuán)隊(duì)在 2021 年 4 月更新 Go 泛型設(shè)計(jì)方案時(shí)引入的。在那一次方案變更中,原方案中用于接口類型中定義類型元素的 type 關(guān)鍵字被去除了,泛型相關(guān)語(yǔ)法得到了進(jìn)一步的簡(jiǎn)化。
一旦確定了一個(gè)接口類型的類型集合,類型集合中的元素就可以滿足以該接口類型作為的類型約束,也就是可以將該集合中的元素作為類型實(shí)參傳遞給該接口類型約束的類型參數(shù)。
那么類型集合究竟是怎么定義的呢?下面我們來(lái)看一下。
結(jié)合 Go 泛型設(shè)計(jì)方案以及Go 語(yǔ)法規(guī)范,我們可以這么來(lái)理解類型集合:
- 每個(gè)類型都有一個(gè)類型集合;
- 非接口類型的類型的類型集合中僅包含其自身,比如非接口類型
T,它的類型集合為{T},即集合中僅有一個(gè)元素且這唯一的元素就是它自身。
但我們最終要搞懂的是用于定義約束的接口類型的類型集合,所以以上這兩點(diǎn)都是在為下面接口類型的類型集合定義做鋪墊,定義如下:
- 空接口類型(
any或interface{})的類型集合是一個(gè)無(wú)限集合,該集合中的元素為所有非接口類型。這個(gè)與我們之前的認(rèn)知也是一致的,所有非接口類型都實(shí)現(xiàn)了空接口類型; - 非空接口類型的類型集合則是其定義中接口元素的類型集合的交集(如下圖)。
由此可見(jiàn),要想確定一個(gè)接口類型的類型集合,我們需要知道其中每個(gè)接口元素的類型集合。
上面我們說(shuō)過(guò),接口元素可以是其他嵌入接口類型,可以是常規(guī)方法元素,也可以是類型元素。當(dāng)接口元素為其他嵌入接口類型時(shí),該接口元素的類型集合就為該嵌入接口類型的類型集合;而當(dāng)接口元素為常規(guī)方法元素時(shí),接口元素的類型集合就為該方法的類型集合。
到這里你可能會(huì)很疑惑:一個(gè)方法也有自己的類型集合?
是的。Go 規(guī)定一個(gè)方法的類型集合為所有實(shí)現(xiàn)了該方法的非接口類型的集合,這顯然也是一個(gè)無(wú)限集合,如下圖所示:
通過(guò)方法元素的類型集合,我們也可以合理解釋僅包含多個(gè)方法的常規(guī)接口類型的類型集合,那就是這些方法元素的類型集合的交集,即所有實(shí)現(xiàn)了這三個(gè)方法的類型所組成的集合。
最后我們?cè)賮?lái)看看類型元素。類型元素的類型集合相對(duì)來(lái)說(shuō)是最好理解的,每個(gè)類型元素的類型集合就是其表示的所有類型組成的集合。如果是 ~T 形式,則集合中不僅包含 T 本身,還包含所有以 T 為底層類型的類型。如果使用 Union element,則類型集合是所有豎線 “|” 連接的類型的類型集合的并集。
接下來(lái),我們來(lái)做個(gè)稍復(fù)雜些的實(shí)例分析,我們來(lái)分析一下下面接口類型I 的類型集合:
type Intf1 interface {
~int | string
F1()
F2()
}
type Intf2 interface {
~int | ~float64
}
type I interface {
Intf1
M1()
M2()
int | ~string | Intf2
}
我們看到,接口類型 I 由四個(gè)接口元素組成,分別是 Intf1、M1、M2 和 Union element “int | ~string | Intf2”,我們只要分別求出這四個(gè)元素的類型集合,再取一個(gè)交集即可。
Intf1的類型集合
Intf1 是接口類型 I 的一個(gè)嵌入接口,它自身也是由三個(gè)接口元素組成,它的類型集合為這三個(gè)接口元素的交集,即 {以 int 為底層類型的所有類型、string、實(shí)現(xiàn)了 F1 和 F2 方法的所有類型}。
- M1 和 M2 的類型集合
就像前面所說(shuō)的,方法的類型集合是由所有實(shí)現(xiàn)該方法的類型組成的,因此 M1 的方法集合為 {實(shí)現(xiàn)了 M1 的所有類型},M2 的方法集合為 {實(shí)現(xiàn)了 M2 的所有類型}。
int | ~string | Intf2的類型集合
這是一個(gè)類型元素,它的類型集合為 int、~string 和 Intf2 類型集合的并集。int 類型集合就是 {int},~string 的類型集合為 {以 string 為底層類型的所有類型},而 Intf2 的類型集合為 {以 int 為底層類型的所有類型,以 float64 為底層類型的所有類型}。
為了更好地說(shuō)明最終類型集合是如何取得的,我們?cè)谙旅嬖倭幸幌赂鱾€(gè)接口元素的類型集合:
-
Intf1的類型集合:{以int為底層類型的所有類型、string、實(shí)現(xiàn)了F1和F2方法的所有類型}; -
M1的類型集合:{實(shí)現(xiàn)了M1的所有類型}; -
M2的類型集合:{實(shí)現(xiàn)了M2的所有類型}; -
int | ~string | Intf2的類型集合:{以int為底層類型的所有類型,以float64為底層類型的所有類型,以string為底層類型的所有類型}。
接下來(lái)我們?nèi)∫幌律厦婕系慕患簿褪?{以 int 為底層類型的且實(shí)現(xiàn)了 F1、F2、M1、M2 這個(gè)四個(gè)方法的所有類型}。
現(xiàn)在我們用代碼來(lái)驗(yàn)證一下:
// typeset.go
func doSomething[T I](t T) {
}
type MyInt int
func (MyInt) F1() {
}
func (MyInt) F2() {
}
func (MyInt) M1() {
}
func (MyInt) M2() {
}
func main() {
var a int = 11
//doSomething(a) //int does not implement I (missing F1 method)
var b = MyInt(a)
doSomething(b) // ok
}
如上代碼,我們定義了一個(gè)以 int 為底層類型的自定義類型 MyInt 并實(shí)現(xiàn)了四個(gè)方法,這樣 MyInt 就滿足了泛型函數(shù) doSomething 中約束 I 的要求,可以作為類型實(shí)參傳遞。
六、簡(jiǎn)化版的約束形式
在前面的介紹和示例中,泛型參數(shù)的約束都是一個(gè)完整的接口類型,要么是獨(dú)立定義在泛型函數(shù)外面(比如下面代碼中的 I 接口),要么以接口字面值的形式,直接放在類型參數(shù)列表中對(duì)類型參數(shù)進(jìn)行約束,比如下面示例中 doSomething2 類型參數(shù)列表中的接口類型字面值:
type I interface { // 獨(dú)立于泛型函數(shù)外面定義
~int | ~string
}
func doSomething1[T I](t T)
func doSomething2[T interface{~int | ~string}](t T) // 以接口類型字面值作為約束
但在約束對(duì)應(yīng)的接口類型中僅有一個(gè)接口元素,且該元素為類型元素時(shí),Go 提供了簡(jiǎn)化版的約束形式,我們不必將約束獨(dú)立定義為一個(gè)接口類型,比如上面的 doSomething2 可以簡(jiǎn)寫(xiě)為下面簡(jiǎn)化形式:
func doSomething2[T ~int | ~string](t T) // 簡(jiǎn)化版的約束形式
你看,這個(gè)簡(jiǎn)化版的約束形式就是去掉了 interface 關(guān)鍵字和外圍的大括號(hào),如果用一個(gè)一般形式來(lái)表述,那就是:
func doSomething[T interface {T1 | T2 | ... | Tn}](t T)
等價(jià)于下面簡(jiǎn)化版的約束形式:
func doSomething[T T1 | T2 | ... | Tn](t T)
這種簡(jiǎn)化形式也可以理解為一種類型約束的語(yǔ)法糖。不過(guò)有一種情況要注意,那就是定義僅包含一個(gè)類型參數(shù)的泛型類型時(shí),如果約束中僅有一個(gè) *int 型類型元素,我們使用上述簡(jiǎn)化版形式就會(huì)有問(wèn)題,比如:
type MyStruct [T * int]struct{} // 編譯錯(cuò)誤:undefined: T
// 編譯錯(cuò)誤:int (type) is not an expression
當(dāng)遇到這種情況時(shí),Go 編譯器會(huì)將該語(yǔ)句理解為一個(gè)類型聲明:MyStruct 為新類型的名字,而其底層類型為 [T *int]struct{},即一個(gè)元素為空結(jié)構(gòu)體類型的數(shù)組。
那么怎么解決這個(gè)問(wèn)題呢?目前有兩種方案,一種是用完整形式的約束:
type MyStruct[T interface{*int}] struct{}
另外一種則是在簡(jiǎn)化版約束的 *int 類型后面加上一個(gè)逗號(hào):
type MyStruct[T *int,] struct{}
七、約束的類型推斷
在大多數(shù)情況下,我們都可以使用類型推斷避免在調(diào)用泛型函數(shù)時(shí)顯式傳入類型實(shí)參,Go 泛型可以根據(jù)泛型函數(shù)的實(shí)參推斷出類型實(shí)參。但當(dāng)我們遇到下面示例中的泛型函數(shù)時(shí),光依靠函數(shù)類型實(shí)參的推斷是無(wú)法完全推斷出所有類型實(shí)參的:
func DoubleDefined[S ~[]E, E constraints.Integer](s S) S {
因?yàn)橄?DoubleDefined 這樣的泛型函數(shù),其類型參數(shù) E 在其常規(guī)參數(shù)列表中并未被用來(lái)聲明輸入?yún)?shù),函數(shù)類型實(shí)參推斷僅能根據(jù)傳入的 S 的類型,推斷出類型參數(shù) S 的類型實(shí)參,E 是無(wú)法推斷出來(lái)的。所以為了進(jìn)一步避免開(kāi)發(fā)者顯式傳入類型實(shí)參,Go 泛型支持了約束類型推斷(constraint type inference),即基于一個(gè)已知的類型實(shí)參(已經(jīng)由函數(shù)類型實(shí)參推斷判斷出來(lái)了),來(lái)推斷其他類型參數(shù)的類型。
我們還以上面 DoubleDefined 這個(gè)泛型函數(shù)為例,當(dāng)通過(guò)實(shí)參推斷得到類型 S 后,Go 會(huì)嘗試啟動(dòng)約束類型推斷來(lái)推斷類型參數(shù) E 的類型。但你可能也看出來(lái)了,約束類型推斷可成功應(yīng)用的前提是 S 是由 E 所表示的。
八、小結(jié)
本文我們先從 Go 泛型內(nèi)置的約束 any 和 comparable 入手,充分了解了約束對(duì)于泛型函數(shù)的類型參數(shù)以及泛型函數(shù)中的實(shí)現(xiàn)代碼的限制與影響。然后,我們了解了如何自定義約束,知道了因?yàn)?Go 不支持操作符重載,單純依賴基于行為的接口類型(僅包含方法元素)作約束是無(wú)法滿足泛型函數(shù)的要求的。這樣我們進(jìn)一步學(xué)習(xí)了 Go 接口類型的擴(kuò)展語(yǔ)法:支持類型元素。
既有方法元素,也有類型元素,對(duì)于作為約束的非基本接口類型,我們就不能像以前那樣僅憑是否實(shí)現(xiàn)方法集合來(lái)判斷是否實(shí)現(xiàn)了該接口,新的判定手段為類型集合。并且,類型集合不是一個(gè)運(yùn)行時(shí)概念,我們目前還無(wú)法通過(guò)運(yùn)行時(shí)反射直觀看到一個(gè)接口類型的類型集合是什么!
Go 內(nèi)置了像 any、comparable 的約束,后續(xù)隨著 Go 核心團(tuán)隊(duì)在 Go 泛型使用上的經(jīng)驗(yàn)的逐漸豐富,Go 標(biāo)準(zhǔn)庫(kù)中會(huì)增加更多可直接使用的約束。原計(jì)劃在 Go 1.18 版本加入 Go 標(biāo)準(zhǔn)庫(kù)的一些泛型約束的定義暫放在了 Go 實(shí)驗(yàn)倉(cāng)庫(kù)中,你可以自行參考。
總結(jié)
以上是生活随笔為你收集整理的Go 泛型之泛型约束的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 面试官:说一下MySQL主从复制的原理?
- 下一篇: svelte的一些基础demo