Go 泛型之泛型約束

来源:https://www.cnblogs.com/taoxiaoxin/archive/2023/12/28/17933493.html
-Advertisement-
Play Games

Go 泛型之泛型約束 目錄Go 泛型之泛型約束一、引入二、最寬鬆的約束:any三、支持比較操作的內置約束:comparable四、自定義約束五、類型集合(type set)六、簡化版的約束形式七、約束的類型推斷八、小結 一、引入 雖然泛型是開發人員表達“通用代碼”的一種重要方式,但這並不意味著所有泛 ...


Go 泛型之泛型約束

目錄

一、引入

雖然泛型是開發人員表達“通用代碼”的一種重要方式,但這並不意味著所有泛型代碼對所有類型都適用。更多的時候,我們需要對泛型函數的類型參數以及泛型函數中的實現代碼設置限制。泛型函數調用者只能傳遞滿足限制條件的類型實參,泛型函數內部也只能以類型參數允許的方式使用這些類型實參值。在 Go 泛型語法中,我們使用類型參數約束(type parameter constraint)(以下簡稱約束)來表達這種限制條件。

約束之於類型參數就好比函數參數列表中的類型之於參數:

函數普通參數在函數實現代碼中可以表現出來的性質與可以參與的運算由參數類型限制,而泛型函數的類型參數就由約束(constraint)來限制。

2018 年 8 月由伊恩·泰勒和羅伯特·格瑞史莫主寫的 Go 泛型第一版設計方案中,Go 引入了 contract 關鍵字來定義泛型類型參數的約束。但經過約兩年的 Go 社區公示和討論,在 2020 年 6 月末發佈的泛型新設計方案中,Go 團隊又放棄了新引入的 contract 關鍵字,轉而採用已有的 interface 類型來替代 contract 定義約束。這一改變得到了 Go 社區的大力支持。使用 interface 類型作為約束的定義方法能夠最大程度地復用已有語法,並抑制語言引入泛型後的複雜度。

但原有的 interface 語法尚不能滿足定義約束的要求。所以,在 Go 泛型版本中,interface 語法也得到了一些擴展,也正是這些擴展給那些剛剛入門 Go 泛型的 Go 開發者帶來了一絲困惑,這也是約束被認為是 Go 泛型的一個難點的原因。

下麵我們來看一下 Go 類型參數的約束, Go 原生內置的約束、如何定義自己的約束、新引入的類型集合概念等。我們先來看一下 Go 語言的內置約束,從 Go 泛型中最寬鬆的約束:any 開始。

二、最寬鬆的約束:any

無論是泛型函數還是泛型類型,其所有類型參數聲明中都必須顯式包含約束,即便你允許類型形參接受所有類型作為類型實參傳入也是一樣。那麼我們如何表達“所有類型”這種約束呢?我們可以使用空介面類型(interface{})來作為類型參數的約束:

func Print[T interface{}](sl []T) {
    // ... ...
}

func doSomething[T1 interface{}, T2 interface{}, T3 interface{}](t1 T1, t2 T2, t3 T3) {
    // ... ...
}

不過使用 interface{} 作為約束至少有以下幾點“不足”:

  • 如果存在多個這類約束時,泛型函數聲明部分會顯得很冗長,比如上面示例中的 doSomething 的聲明部分;
  • interface{} 包含 {} 這樣的符號,會讓本已經很複雜的類型參數聲明部分顯得更加複雜;
  • comparableSortableordered 這樣的約束命名相比,interface{} 作為約束的表意不那麼直接。

為此,Go 團隊在 Go 1.18 泛型落地的同時又引入了一個預定義標識符:anyany 本質上是 interface{} 的一個類型別名:

// $GOROOT/src/builtin/buildin.go
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

這樣,我們在泛型類型參數聲明中就可以使用 any 替代 interface{},而上述 interface{} 作為類型參數約束的幾點“不足”也隨之被消除掉了。

any 約束的類型參數意味著可以接受所有類型作為類型實參。在函數體內,使用 any 約束的形參 T 可以用來做如下操作:

  • 聲明變數
  • 同類型賦值
  • 將變數傳給其他函數或從函數返回
  • 取變數地址
  • 轉換或賦值給 interface{} 類型變數
  • 用在類型斷言或 type switch 中
  • 作為複合類型中的元素類型
  • 傳遞給預定義的函數,比如 new

下麵是 any 約束的類型參數執行這些操作的一個示例:

// 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)            // 傳給其他函數

    p := &a         // 取變數地址
    _ = p

    var i interface{} = a  // 轉換或賦值給interface{}類型變數
    _ = i

    c := new(T1)    // 傳遞給預定義函數
    _ = c

    f(a)            // 將變數傳給其他函數

    sl := make([]T1, 0, 10) // 作為複合類型中的元素類型
    _ = sl

    j, ok := i.(T1) // 用在類型斷言中
    _ = ok
    _ = j

    switch i.(type) { // 作為type switch中的case類型
    case T1:
    case T2:
    }
    return a        // 從函數返回
}

但如果對 any 約束的類型參數進行了非上述允許的操作,比如相等性或不等性比較,那麼 Go 編譯器就會報錯:

// any.go

func doSomething[T1, T2 any](t1 T1, t2 T2) T1 {
    var a T1 
    if a == t1 { // 編譯器報錯:invalid operation: a == t1 (incomparable types in type set)
    }
    
    if a != t1 { // 編譯器報錯:invalid operation: a != t1 (incomparable types in type set)
    }
    ... ...
}

所以說,如果我們想在泛型函數體內部對類型參數聲明的變數實施相等性(==)或不等性比較(!=)操作,我們就需要更換約束,這就引出了 Go 內置的另外一個預定義約束:comparable

三、支持比較操作的內置約束:comparable

Go 泛型提供了預定義的約束: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 }

不過從上述這行源碼我們仍然無法直觀看到 comparable 的實現細節,Go 編譯器會在編譯期間判斷某個類型是否實現了 comparable 介面。

根據其註釋說明,所有可比較的類型都實現了 comparable 這個介面,包括:布爾類型、數值類型、字元串類型、指針類型、channel 類型、元素類型實現了 comparable 的數組和成員類型均實現了 comparable 介面的結構體類型。下麵的例子可以讓我們直觀地看到這一點:

// 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 結構體類型因為內含不支持比較的切片類型,被 Go 編譯器認為未實現 comparable 介面,但除此之外的其他類型作為類型實參都滿足 comparable 約束的要求。

此外還要註意,comparable 雖然也是一個 interface,但它不能像普通 interface 類型那樣來用,比如下麵代碼會導致編譯器報錯:

var i comparable = 5 // 編譯器錯誤:cannot use type comparable outside a type constraint: interface is (or embeds) comparable

從編譯器的錯誤提示,我們看到:comparable 只能用作修飾類型參數的約束。

四、自定義約束

我們知道,Go 泛型最終決定使用 interface 語法來定義約束。這樣一來,凡是介面類型均可作為類型參數的約束。下麵是一個使用普通介面類型作為類型參數約束的示例:

// 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]
}

這個例子中,我們使用的是 fmt.Stringer 介面作為約束。一方面,這要求類型參數 T 的實參必須實現 fmt.Stringer 介面的所有方法;另一方面,泛型函數 Stringify 的實現代碼中,聲明的 T 類型實例(比如 v)也僅被允許調用 fmt.StringerString 方法。

這類基於行為(方法集合)定義的約束對於習慣了 Go 介面類型的開發者來說,是相對好理解的。定義和使用起來,與下麵這樣的以介面類型作為形參的普通 Go 函數相比,區別似乎不大:

func Stringify(s []fmt.Stringer) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

但現在我想擴展一下上面 stringify.go 這個示例,將 Stringify 的語義改為只處理非零值的元素:

// stringify_without_zero.go

func StringifyWithoutZero[T fmt.Stringer](s []T) (ret []string) {
    var zero T
    for _, v := range s {
        if v == zero { // 編譯器報錯:invalid operation: v == zero (incomparable types in type set)
            continue
        }
        ret = append(ret, v.String())
    }
    return ret
}

我們看到,針對 v 的相等性判斷導致了編譯器報錯,我們需要為類型參數賦予更多的能力,比如支持相等性和不等性比較。這讓我們想起了我們剛剛學過的 Go 內置約束 comparable,實現 comparable 的類型,便可以支持相等性和不等性判斷操作了。

我們知道,comparable 雖然不能像普通介面類型那樣聲明變數,但它卻可以作為類型嵌入到其他介面類型中,下麵我們就擴展一下上面示例:

// 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)
}

在這個示例里,我們自定義了一個 Stringer 介面類型作為約束。在該類型中,我們不僅定義了 String 方法,還嵌入了 comparable,這樣在泛型函數中,我們用 Stringer 約束的類型參數就具備了進行相等性和不等性比較的能力了!

但我們的示例演進還沒有完,現在相等性和不等性比較已經不能滿足我們需求了,我們還要為之加上對排序行為的支持,並基於排序能力實現下麵的 StringifyLessThan 泛型函數:

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
}

但現在當我們編譯上面 StringifyLessThan 函數時,我們會得到編譯器的報錯信息 invalid operation: v >= max (type parameter T is not comparable with >=)。Go 編譯器認為 Stringer 約束的類型參數 T 不具備排序比較能力。

如果連排序比較性都無法支持,這將大大限制我們泛型函數的表達能力。但是 Go 又不支持運算符重載(operator overloading),不允許我們定義出下麵這樣的介面類型作為類型參數的約束:

type Stringer[T any] interface {
    String() string
    comparable
  >(t T) bool
  >=(t T) bool
  <(t T) bool
  <=(t T) bool
}

那我們又該如何做呢?別擔心,Go 核心團隊顯然也想到了這一點,於是對 Go 介面類型聲明語法做了擴展,支持在介面類型中放入類型元素(type element)信息,比如下麵的 ordered 介面類型:

type ordered interface {
  ~int | ~int8 | ~int16 | ~int32 | ~int64 |
  ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
  ~float32 | ~float64 | ~string
}

在這個介面類型的聲明中,我們沒有看到任何方法,取而代之的是一組由豎線 “|” 分隔的、帶著小尾巴 “~” 的類型列表。這個列表表示的是,以它們為底層類型(underlying type)的類型都滿足 ordered 約束,都可以作為以 ordered 為約束的類型參數的類型實參,傳入泛型函數。

我們將其組合到我們聲明的 Stringer 介面中,然後應用一下我們的 StringifyLessThan 函數:

type Stringer interface {
    ordered
    comparable
    String() string
}

func main() {
    sl := StringifyLessThan([]MyString{"I", "", "love", "", "golang"}, MyString("cpp")) // 輸出:[I]
    fmt.Println(sl)
}

這回編譯器沒有報錯,並且程式輸出了預期的結果。

好了,看了那麼多例子,是時候正式對 Go 介面類型語法的擴展做一個說明瞭。下麵是擴展後的介面類型定義的組成示意圖:

我們看到,新的介面類型依然可以嵌入其他介面類型,滿足組合的設計哲學;除了嵌入的其他介面類型外,其餘的組成元素被稱為介面元素(interface element)。

介面元素也有兩類,一類就是常規的方法元素(method element),每個方法元素對應一個方法原型;另一類則是此次擴展新增的類型元素(type element),即在介面類型中,我們可以放入一些類型信息,就像前面的 ordered 介面那樣。

類型元素可以是單個類型,也可以是一組由豎線 “|” 連接的類型,豎線 “|” 的含義是“並”,這樣的一組類型被稱為 union element。無論是單個類型,還是 union element 中由 “|” 分隔的類型,如果類型中不帶有 “~” 符號的類型就代表其自身;而帶有 “~” 符號的類型則代表以該類型為底層類型(underlying type)的所有類型,這類帶有 “~” 的類型也被稱為 approximation element,如下麵示例:

type Ia interface {
  int | string  // 僅代表int和string
}

type Ib interface {
  ~int | ~string  // 代表以int和string為底層類型的所有類型
}

下圖是類型元素的分解說明,供你參考:

不過要註意的是:union element 中不能包含帶有方法元素的介面類型,也不能包含預定義的約束類型,如 comparable

擴展後,Go 將介面類型分成了兩類,一類是基本介面類型(basic interface type),即其自身和其嵌入的介面類型都只包含方法元素,而不包含類型元素。基本介面類型不僅可以當做常規介面類型來用,即聲明介面類型變數、介面類型變數賦值等,還可以作為泛型類型參數的約束。

除此之外的非空介面類型都屬於非基本介面類型,即直接或間接(通過嵌入其他介面類型)包含了類型元素的介面類型。這類介面類型僅可以用作泛型類型參數的約束,或被嵌入到其他僅作為約束的介面類型中,下麵的代碼就很直觀地展示了這兩種介面類型的特征:

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 // 基本介面類型支持常規用法
    var nbi NonBasicInterface = s // 非基本介面不支持常規用法,導致編譯器錯誤:cannot use type NonBasicInterface outside a type constraint: interface contains type constraints
    bi.M1()
    nbi.M1()
    foo(s)
    bar(s)           
}

看到這裡,你可能會覺得有問題了:基本介面類型,由於其僅包含方法元素,我們依舊可以基於之前講過的方法集合,來確定一個類型是否實現了介面,以及是否可以作為類型實參傳遞給約束下的類型形參。但對於只能作為約束的非基本介面類型,既有方法元素,也有類型元素,我們如何判斷一個類型是否滿足約束,並作為類型實參傳給類型形參呢?

這時候我們就需要 Go 泛型落地時引入的新概念:類型集合(type set),類型集合將作為後續判斷類型是否滿足約束的基本手段。

五、類型集合(type set)

類型集合(type set)的概念是 Go 核心團隊在 2021 年 4 月更新 Go 泛型設計方案時引入的。在那一次方案變更中,原方案中用於介面類型中定義類型元素的 type 關鍵字被去除了,泛型相關語法得到了進一步的簡化。

一旦確定了一個介面類型的類型集合,類型集合中的元素就可以滿足以該介面類型作為的類型約束,也就是可以將該集合中的元素作為類型實參傳遞給該介面類型約束的類型參數。

那麼類型集合究竟是怎麼定義的呢?下麵我們來看一下。

結合 Go 泛型設計方案以及Go 語法規範,我們可以這麼來理解類型集合:

  • 每個類型都有一個類型集合;
  • 非介面類型的類型的類型集合中僅包含其自身,比如非介面類型 T,它的類型集合為 {T},即集合中僅有一個元素且這唯一的元素就是它自身。

但我們最終要搞懂的是用於定義約束的介面類型的類型集合,所以以上這兩點都是在為下麵介面類型的類型集合定義做鋪墊,定義如下:

  • 空介面類型(anyinterface{})的類型集合是一個無限集合,該集合中的元素為所有非介面類型。這個與我們之前的認知也是一致的,所有非介面類型都實現了空介面類型;
  • 非空介面類型的類型集合則是其定義中介面元素的類型集合的交集(如下圖)。

由此可見,要想確定一個介面類型的類型集合,我們需要知道其中每個介面元素的類型集合。

上面我們說過,介面元素可以是其他嵌入介面類型,可以是常規方法元素,也可以是類型元素。當介面元素為其他嵌入介面類型時,該介面元素的類型集合就為該嵌入介面類型的類型集合;而當介面元素為常規方法元素時,介面元素的類型集合就為該方法的類型集合。

到這裡你可能會很疑惑:一個方法也有自己的類型集合?

是的。Go 規定一個方法的類型集合為所有實現了該方法的非介面類型的集合,這顯然也是一個無限集合,如下圖所示:

通過方法元素的類型集合,我們也可以合理解釋僅包含多個方法的常規介面類型的類型集合,那就是這些方法元素的類型集合的交集,即所有實現了這三個方法的類型所組成的集合。

最後我們再來看看類型元素。類型元素的類型集合相對來說是最好理解的,每個類型元素的類型集合就是其表示的所有類型組成的集合。如果是 ~T 形式,則集合中不僅包含 T 本身,還包含所有以 T 為底層類型的類型。如果使用 Union element,則類型集合是所有豎線 “|” 連接的類型的類型集合的並集。

接下來,我們來做個稍複雜些的實例分析,我們來分析一下下麵介面類型I 的類型集合:

type Intf1 interface {
    ~int | string
  F1()
  F2()
}

type Intf2 interface {
  ~int | ~float64
}

type I interface {
    Intf1 
    M1()
    M2()
    int | ~string | Intf2
}

我們看到,介面類型 I 由四個介面元素組成,分別是 Intf1M1M2Union element “int | ~string | Intf2”,我們只要分別求出這四個元素的類型集合,再取一個交集即可。

  • Intf1 的類型集合

Intf1 是介面類型 I 的一個嵌入介面,它自身也是由三個介面元素組成,它的類型集合為這三個介面元素的交集,即 {以 int 為底層類型的所有類型、string、實現了 F1 和 F2 方法的所有類型}

  • M1 和 M2 的類型集合

就像前面所說的,方法的類型集合是由所有實現該方法的類型組成的,因此 M1 的方法集合為 {實現了 M1 的所有類型}M2 的方法集合為 {實現了 M2 的所有類型}

  • int | ~string | Intf2 的類型集合

這是一個類型元素,它的類型集合為 int~stringIntf2 類型集合的並集。int 類型集合就是 {int}~string 的類型集合為 {以 string 為底層類型的所有類型},而 Intf2 的類型集合為 {以 int 為底層類型的所有類型,以 float64 為底層類型的所有類型}

為了更好地說明最終類型集合是如何取得的,我們在下麵再列一下各個介面元素的類型集合:

  • Intf1 的類型集合:{以 int 為底層類型的所有類型、string、實現了 F1F2 方法的所有類型};
  • M1 的類型集合:{實現了 M1 的所有類型};
  • M2 的類型集合:{實現了 M2 的所有類型};
  • int | ~string | Intf2 的類型集合:{以 int 為底層類型的所有類型,以 float64 為底層類型的所有類型,以 string 為底層類型的所有類型}

接下來我們取一下上面集合的交集,也就是 {以 int 為底層類型的且實現了 F1F2M1M2 這個四個方法的所有類型}。

現在我們用代碼來驗證一下:

// 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
}

如上代碼,我們定義了一個以 int 為底層類型的自定義類型 MyInt 並實現了四個方法,這樣 MyInt 就滿足了泛型函數 doSomething 中約束 I 的要求,可以作為類型實參傳遞。

六、簡化版的約束形式

在前面的介紹和示例中,泛型參數的約束都是一個完整的介面類型,要麼是獨立定義在泛型函數外面(比如下麵代碼中的 I 介面),要麼以介面字面值的形式,直接放在類型參數列表中對類型參數進行約束,比如下麵示例中 doSomething2 類型參數列表中的介面類型字面值:

type I interface { // 獨立於泛型函數外面定義
    ~int | ~string
}

func doSomething1[T I](t T)
func doSomething2[T interface{~int | ~string}](t T) // 以介面類型字面值作為約束

在約束對應的介面類型中僅有一個介面元素,且該元素為類型元素時,Go 提供了簡化版的約束形式,我們不必將約束獨立定義為一個介面類型,比如上面的 doSomething2 可以簡寫為下麵簡化形式:

func doSomething2[T ~int | ~string](t T) // 簡化版的約束形式

你看,這個簡化版的約束形式就是去掉了 interface 關鍵字和外圍的大括弧,如果用一個一般形式來表述,那就是:

func doSomething[T interface {T1 | T2 | ... | Tn}](t T)

等價於下麵簡化版的約束形式:

func doSomething[T T1 | T2 | ... | Tn](t T) 

這種簡化形式也可以理解為一種類型約束的語法糖。不過有一種情況要註意,那就是定義僅包含一個類型參數的泛型類型時,如果約束中僅有一個 *int 型類型元素,我們使用上述簡化版形式就會有問題,比如:

type MyStruct [T * int]struct{} // 編譯錯誤:undefined: T
                                // 編譯錯誤:int (type) is not an expression

當遇到這種情況時,Go 編譯器會將該語句理解為一個類型聲明:MyStruct 為新類型的名字,而其底層類型為 [T *int]struct{},即一個元素為空結構體類型的數組。

那麼怎麼解決這個問題呢?目前有兩種方案,一種是用完整形式的約束:

type MyStruct[T interface{*int}] struct{} 

另外一種則是在簡化版約束的 *int 類型後面加上一個逗號:

type MyStruct[T *int,] struct{} 

七、約束的類型推斷

在大多數情況下,我們都可以使用類型推斷避免在調用泛型函數時顯式傳入類型實參,Go 泛型可以根據泛型函數的實參推斷出類型實參。但當我們遇到下麵示例中的泛型函數時,光依靠函數類型實參的推斷是無法完全推斷出所有類型實參的:

func DoubleDefined[S ~[]E, E constraints.Integer](s S) S {

因為像 DoubleDefined 這樣的泛型函數,其類型參數 E 在其常規參數列表中並未被用來聲明輸入參數,函數類型實參推斷僅能根據傳入的 S 的類型,推斷出類型參數 S 的類型實參,E 是無法推斷出來的。所以為了進一步避免開發者顯式傳入類型實參,Go 泛型支持了約束類型推斷(constraint type inference),即基於一個已知的類型實參(已經由函數類型實參推斷判斷出來了),來推斷其他類型參數的類型。

我們還以上面 DoubleDefined 這個泛型函數為例,當通過實參推斷得到類型 S 後,Go 會嘗試啟動約束類型推斷來推斷類型參數 E 的類型。但你可能也看出來了,約束類型推斷可成功應用的前提是 S 是由 E 所表示的。

八、小結

本文我們先從 Go 泛型內置的約束 anycomparable 入手,充分瞭解了約束對於泛型函數的類型參數以及泛型函數中的實現代碼的限制與影響。然後,我們瞭解瞭如何自定義約束,知道了因為 Go 不支持操作符重載,單純依賴基於行為的介面類型(僅包含方法元素)作約束是無法滿足泛型函數的要求的。這樣我們進一步學習了 Go 介面類型的擴展語法:支持類型元素

既有方法元素,也有類型元素,對於作為約束的非基本介面類型,我們就不能像以前那樣僅憑是否實現方法集合來判斷是否實現了該介面,新的判定手段為類型集合。並且,類型集合不是一個運行時概念,我們目前還無法通過運行時反射直觀看到一個介面類型的類型集合是什麼!

Go 內置了像 anycomparable 的約束,後續隨著 Go 核心團隊在 Go 泛型使用上的經驗的逐漸豐富,Go 標準庫中會增加更多可直接使用的約束。原計劃在 Go 1.18 版本加入 Go 標準庫的一些泛型約束的定義暫放在了 Go 實驗倉庫中,你可以自行參考。

分享是一種快樂,開心是一種態度!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. 需求分析 為了提高用戶對頁面鏈接分享的體驗,需要對分享鏈接做一些處理。 以 Telegram(國外某一通訊軟體) 為例,當在 Telegram 上分享已做過處理的鏈接時,它會自動嘗試獲取鏈接的預覽信息,包括標題、描述和圖片。 如此當 ...
  • $0 和 __vue__ $0 是指當滑鼠點擊 Element 面板的某個 dom 元素後,console 里 $0 變數會自動指向該 dom 元素對象 __vue__ 是指 vue 框架會往 vue 組件 $mount 掛載的 dom 元素對象上添加一個 __vue__ 變數來指向當前 vue 組 ...
  • npm導入和風天氣的圖標庫後使用沒有效果,就在網上查詢了下怎麼解決,然後動手嘗試一下。 參考文章 步驟 1. 下載圖標文件(鏈接),解壓後大致這樣 2. 在transfonter網站將需要的圖標字體轉成Base64,在font\fonts文件下 選擇上傳 下載 3. 解壓後把stylesheet.c ...
  • keycloak~從login-status-iframe相關文章,可閱讀我的這兩篇keycloak~從login-status-iframe頁面總結如何跨域傳值 ,keycloak~對接login-status-iframe頁面判斷用戶狀態變更 。 什麼是跨域 跨域(Cross-Origin)是指 ...
  • 本文首發於公眾號:Hunter後端 原文鏈接:Python筆記一之excel的讀取 這裡我常用的 python 對於 excel 的讀取庫有兩個,一個是 xlsxwriter 用於操作 excel 的寫入,一個是 xlrd 用於 excel 文件的讀取。 使用的庫的版本如下: xlsx==1.2.6 ...
  • 第十九章介紹了Jasypt,用於在Spring Boot應用中加密敏感信息。通過jasypt-spring-boot-starter依賴項,配置加密演算法和密碼,並使用StringEncryptor加密和解密。加密後的信息可嵌入屬性文件中,提高資料庫密碼等敏感信息的安全性。加解密基於密鑰,建議將密鑰通... ...
  • 相較於BarCode,QRCode有明顯的特征區域,也就是左上角、右上角、左下角三個”回“字區域,得益於hierarchy中,父子關係的輪廓是連續的(下標),所以這個時候我們就可以通過cv2.findContours()返回的hierarchy來進行定位。 我們直接上代碼 1 import cv2 ...
  • C++作為一門靜態類型語言,是需要程式員聲明變數類型的。然而來到了C++11,auto的誕生使得變數聲明變得及為方便,尤其是對於比較長的模板類型,auto一定程度上為代碼編寫者減輕了負擔。到了C++23,突然來了個新特性:auto{x}/auto(x),這又是個什麼東西,它的motivation又是 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...