泛型來了,看看如何應用到 slice

JCSXZ_12發表於2021-03-02

原文地址:https://eli.thegreenplace.net/2021/generic-functions-on-slices-with-go-type-parameters/

原文作者: Eli Bendersky

本文永久連結:https://github.com/gocn/translator/blob/master/2021/w10_Generic_functions_on_slices_with_Go_type_parameters.md

譯者:Jancd

經過多年的努力,Go 的泛型提案已在本週被接受!對於 Go 社群來說,這是個好訊息。Go 增加泛型特性這個事情最早能追溯到在 Go 1.0 釋出之前的 2010 年。

我認為當前的這個提案在表達能力和可理解性之間取得了很好的平衡。 它應該能讓 Go 程式設計師表達 95% 泛型最需要的東西,同時也讓編寫那些在其他語言中被泛型貶損的難以理解的程式碼變得困難或不可能。目前,Go 團隊正致力於在 1.18 版本中引入泛型(測試版將於 2021 年 12 月釋出),儘管這些時間表還沒有最終敲定。

上個月我寫了一篇關於為什麼在 Go 中在切片上編寫泛型函式是困難的文章。為了慶祝這個提案被接受的里程碑,本篇博文展示了一旦這個泛型提案進入 Go,這將是一個不存在的問題。

切片的泛型函式?

讓我們從定義問題開始;Ian Lance Taylor 有一篇非常棒的演講和部落格文章叫做 “為什麼需要泛型?”,我建議你先看看。

我將使用 Ian 的翻轉切片函式作為示例,並將快速地討論在那次演講中已經涉及到的主題。

假設我們想寫一個函式來反轉 Go 中的切片;我們可以從一個具體的函式開始,比如下面的反轉整型數的切片:

func ReverseInts(s []int) {
  first := 0
  last := len(s) - 1
  for first < last {
    s[first], s[last] = s[last], s[first]
    first++
    last--
  }
}

那如何反轉一個字串切片?

func ReverseStrings(s []string) {
  first := 0
  last := len(s) - 1
  for first < last {
    s[first], s[last] = s[last], s[first]
    first++
    last--
  }
}

emm,它看起來和上面的反轉整形的函式很像,總結起來就是型別由 int->string簡單替換。那麼問題來了,這個函式不能只寫一次嗎?

我們再看一個例子,Go 通過 interface 允許多型性;讓我們試著寫一個 “泛型” 函式來反轉任何型別的切片:

func ReverseAnything(s []interface{}) {
  first := 0
  last := len(s) - 1
  for first < last {
    s[first], s[last] = s[last], s[first]
    first++
    last--
  }
}

我們可以這樣呼叫它,並且結果預期:

iints := []interface{}{2, 3, 4, 5}
ReverseAnything(iints)

istrings := []interface{}{"joe""mike""hello"}
ReverseAnything(istrings)

那這就是我們想要的答案麼?那麼 Go 是不是一直都有泛型?雖然 ReverseAnything 會如期地反轉 interface{} 切片,但在 Go 中我們通常不會在這樣的切片中儲存資料。理論上,我們可以這樣做,但這將放棄大部分的 Go 靜態型別,因為它在任何時候都需要依賴於 (執行時) 型別斷言 [1]

如果我們可以將[]int傳遞給 ReverseAnything,那一切好說。 但這是不可能的,原因有很多。

此外,我們也可以在反轉之前將 []int 複製到 []interface{} 中,但這會有很多缺點:

更多的程式碼:我們必須將切片複製到[]interface{} 中,然後呼叫反轉函式,然後將結果複製回 []int中,而不是反轉切片。 效率 - 大量的資料複製和分配新的切片,而簡單的呼叫 ReverseInts(intslice) 是一個零分配的單一迴圈,也沒有不必要的拷貝。 我們還可以採用其他方法,如程式碼生成,但這些方法存在不同的問題,又會增加問題的複雜度。

所以我們需要型別引數提案。

編寫帶有型別引數的泛型程式碼

使用型別引數提案,編寫一個通用的切片反轉函式將很簡單:

func ReverseSlice[T any](s []T) {
  first := 0
  last := len(s) - 1
  for first < last {
    s[first], s[last] = s[last], s[first]
    first++
    last--
  }
}

函式名後面的方括號區域為函式的使用定義了一個型別引數。[T any] 表示 T 是一個型別形參,可以是任何型別。毫無疑問,函式體與我們的非泛型版本完全相同。

下面是我們如何使用它:

s := []int{2, 4, 8, 11}
ReverseSlice(s)

ss := []string{"joe""mike""hello"}
ReverseSlice(ss)

得益於型別推斷,當我們呼叫ReverseSlice 時,我們不需要指定型別引數 (實際上,在絕大多數其他情況下都是可行的)。

我不會詳細介紹編譯器是如何實現這一點的,因為實現細節仍在變化中。此外,不同的 Go 編譯器可能會選擇以不同的方式來實現這一點,那樣挺好的。

但是,我將強調該建議的一個重要方面:型別引數的值並沒有 'boxed'【譯註:可理解為記憶體堆上分配】。這對效率有重要的影響!這意味著不管通用函式增加了什麼開銷 (就執行時和記憶體佔用而言),它都可能是一個恆定的開銷,而不是一個與切片大小有關的函式。

更多泛型切片函式的例子

型別引數最終允許程式設計師編寫像 mapreducefilter 這樣的泛型函式!無論你是否認為這些函式在風格上適合 Go,它們都很好地展示了 Go 中這個新功能的能力。讓我們以 map 為例:

func Map[T, U any](s []T, f func(T) U) []U {
  r := make([]U, len(s))
  for i, v := range s {
    r[i] = f(v)
  }
  return r
}

它由兩種型別引數化 —— 一種用於 slice 元素,另一種用於返回的 slice 元 素。下面是一個假設的使用場景:

s := []int{2, 4, 8, 11}
ds := Map(s, func(i int) string {return strconv.Itoa(2*i)})

對映函式接受 int 並返回 string。在呼叫 Map 時,這足以讓 Go 的型別推斷理解 TintUstring,而且我們不需要顯式地指定任何型別。ds 被推斷為 []string

當然,我們也可以將 Map 用於標準庫中的現有函式,例如:

names := []string{"joe""mike""sue"}
namesUpper := Map(names, strings.ToUpper)

Filter 示例:

func Filter[T any](s []T, f func(T) bool) []T {
  var r []T
  for _, v := range s {
    if f(v) {
      r = append(r, v)
    }
  }
  return r
}

我們可以這樣呼叫它:

evens := Filter(s, func(i int) bool {return i % 2 == 0})

最後是 Reduce 示例:

func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
  r := init
  for _, v := range s {
    r = f(r, v)
  }
  return r
}

示例使用:

product := Reduce(s, 1, func(a, b int) int {return a*b})

馬上嘗試型別引數

雖然泛型在 1.18 之前無法在 Go 中使用,但你今天就可以嘗試我貼在這篇文章中的所有程式碼 (以及任何你喜歡的程式碼),有幾種方法。

嘗試小片段的最簡單的方法是在 go2go 版本的 Go Playground。它與 Go 工具鏈的型別引數開發分支保持了合理的同步。

要想嘗新或編寫更實質性的程式碼,你可以:

克隆 Go 倉庫 (按照這些說明)。 切換到 dev.go2go 分支. 構建工具鏈(在步驟 1 的連結中也有詳細描述) 使用工具 go2go 執行程式碼示例。 在本文附帶的程式碼倉庫中,你可以找到一個簡單的 bash 指令碼,它可以正確地設定 e nv vars 以執行步驟 4。你可按需使用。

當你克隆 repo 並切換至 dev.go2go 分支後,建議檢視 src/cmd/go2go/testdata/go2path/src 目錄。它包含了許多使用型別引數的泛型 Go 程式碼示例,這些示例非常值得研究。

更多原創文章乾貨分享,請關注公眾號
  • 泛型來了,看看如何應用到 slice
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章