偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Go 泛型接口的正確打開方式,看看幾個(gè)例子!

開發(fā) 前端
自從 Go 引入泛型以來,大家在泛型上最常討論的點(diǎn)之一就是?如何設(shè)計(jì)/使用約束(constraints)。甚至連 Go 官方都出了多篇博文來頻頻介紹這個(gè)知識點(diǎn)。

自從 Go 引入泛型以來,大家在泛型上最常討論的點(diǎn)之一就是 如何設(shè)計(jì)/使用約束(constraints)。

甚至連 Go 官方都出了多篇博文來頻頻介紹這個(gè)知識點(diǎn)。今天就來源于:

圖片圖片

我們知道泛型的類型參數(shù)可以被限制在 cmp.Ordered、comparable 之類的集合:

圖片圖片


但有一個(gè)容易被忽視的事實(shí)是:接口本身也是類型,它們也能有類型參數(shù)

這意味著我們可以用 “泛型接口” 來更優(yōu)雅地表達(dá)某些約束關(guān)系,尤其是涉及 元素自身比較、組合約束、指針接收者 這些場景。

下面我們就結(jié)合以下幾個(gè)例子,結(jié)合著來快速理解這個(gè)特性。

從一個(gè)樹開始:比較與約束

假設(shè)我們要寫一個(gè)泛型二叉搜索樹。

最簡單的做法是直接用 cmp.Ordered

type Tree[E cmp.Ordered] struct {
    root *node[E]
}

func (t *Tree[E]) Insert(element E) {
    t.root = t.root.insert(element)
}

type node[E cmp.Ordered] struct {
    value E
    left  *node[E]
    right *node[E]
}

func (n *node[E]) insert(element E) *node[E] {
    if n == nil {
        return &node[E]{value: element}
    }
    switch {
    case element < n.value:
        n.left = n.left.insert(element)
    case element > n.value:
        n.right = n.right.insert(element)
    }
    return n
}

這樣寫很直觀,但有個(gè)問題:它只適用于內(nèi)置可比較的類型(int、string 等)。

如果我想存 time.Time 呢?就用不了了。

常見解法是要求用戶傳一個(gè)比較函數(shù):

type FuncTree[E any] struct {
    root *funcNode[E]
    cmp  func(E, E) int
}

func NewFuncTree[E any](cmp func(E, E) int) *FuncTree[E] {
    return &FuncTree[E]{cmp: cmp}
}

func (t *FuncTree[E]) Insert(element E) {
    t.root = t.root.insert(t.cmp, element)
}

這當(dāng)然能跑,但有兩個(gè)缺點(diǎn):

  • 必須顯式初始化,不能用零值直接用。
  • 調(diào)用 cmp 是函數(shù)調(diào)用,編譯器不太好內(nèi)聯(lián),性能可能受影響。

能不能換個(gè)思路?答案就是:泛型接口。

用泛型接口表達(dá) “自比較”

如果我們定義一個(gè)接口:

type Comparer interface {
    Compare(Comparer) int
}

看似不錯(cuò),但寫起來很尷尬:方法參數(shù)是 Comparer,每個(gè)類型都要強(qiáng)轉(zhuǎn)回來,非常不 Go。

改進(jìn)后:

type Comparer[T any] interface {
    Compare(T) int
}

這樣就好多了。比如 time.Time 有個(gè) Compare(Time) int 方法,自然就實(shí)現(xiàn)了 Comparer[time.Time]。

進(jìn)一步,我們就能寫一個(gè)支持自比較的樹:

type MethodTree[E Comparer[E]] struct {
    root *methodNode[E]
}

func (t *MethodTree[E]) Insert(element E) {
    t.root = t.root.insert(element)
}

type methodNode[E Comparer[E]] struct {
    value E
    left  *methodNode[E]
    right *methodNode[E]
}

func (n *methodNode[E]) insert(element E) *methodNode[E] {
    if n == nil {
        return &methodNode[E]{value: element}
    }
    sign := element.Compare(n.value)
    switch {
    case sign < 0:
        n.left = n.left.insert(element)
    case sign > 0:
        n.right = n.right.insert(element)
    }
    return n
}

好處是:

  • time.Time 這種自帶 Compare 方法的類型直接能用。
  • 容器仍然支持零值初始化。

再和 map 結(jié)合:需要 comparable

如果我們想基于樹實(shí)現(xiàn)一個(gè) OrderedSet,里面加個(gè) map 來做 O(1) 查詢:

type OrderedSet[E Comparer[E]] struct {
    tree     MethodTree[E]
    elements map[E]bool
}

func (s *OrderedSet[E]) Has(e E) bool {
    return s.elements[e]
}

結(jié)果編譯報(bào)錯(cuò):

invalid map key type E (missing comparable constraint)

因?yàn)?Go 要求 map key 必須是 comparable

這時(shí)有三種寫法:

  1. 在 Comparer 接口里直接嵌入 comparable。
  2. 定義一個(gè)新的 ComparableComparer 接口。
  3. 在 OrderedSet 類型參數(shù)約束里 inline:
type OrderedSet[E interface {
    comparable
    Comparer[E]
}] struct { ... }

哪種方式用,看團(tuán)隊(duì)習(xí)慣,但推薦避免不必要的全局約束,盡量在具體類型上加。

泛型接口不必過度約束

再看一個(gè)常見場景。

定義一個(gè)通用集合接口:

type Set[E any] interface {
    Insert(E)
    Delete(E)
    Has(E) bool
    All() iter.Seq[E]
}

這里的泛型參數(shù)最好只約束成 any,而不是強(qiáng)加 comparable 或 Comparer,因?yàn)椴煌瑢?shí)現(xiàn)有不同需求。

例如基于 map 的實(shí)現(xiàn)必須要求 comparable,而基于 Tree 的則不需要。

指針接收者的坑

如果我們用 Set 來實(shí)現(xiàn)一個(gè)去重函數(shù) Unique,會(huì)遇到一個(gè)尷尬點(diǎn):

  • 有些實(shí)現(xiàn)(比如 OrderedSet)的方法用指針接收者。
  • 如果我們在泛型函數(shù)里聲明 var seen S,當(dāng) S 是 *OrderedSet 時(shí),它會(huì)被初始化成 nil,調(diào)用就 panic。

解決方案是:用一個(gè)額外的類型參數(shù)約束“必須是某個(gè)類型的指針”:

type PtrToSet[S, E any] interface {
    *S
    Set[E]
}

func Unique[E, S any, PS PtrToSet[S, E]](...) { ... }

這樣寫雖然麻煩,但至少能表達(dá)出“S 的指針實(shí)現(xiàn)了 Set”這個(gè)語義。Go 編譯器還能幫我們推斷最后一個(gè)參數(shù),使用時(shí)還算順手。

是否要約束指針接收者?

這類寫法會(huì)讓函數(shù)簽名變得很 “嚇人”。很多時(shí)候,我們其實(shí)不需要這么復(fù)雜的約束,可以換個(gè)角度:

例如,我們本來寫 Unique 想返回一個(gè) iter.Seq[E],但其實(shí)它內(nèi)部要構(gòu)建一個(gè)集合,結(jié)果已經(jīng)全存下來了,那干脆讓調(diào)用方自己傳 Set 進(jìn)來就好:

func InsertAll[E any](set Set[E], seq iter.Seq[E]) {
    for v := range seq {
        set.Insert(v)
    }
}

這樣簡單明了,還能讓不同實(shí)現(xiàn)的 Set 都能復(fù)用。

例如,最簡單的 map 版:

type HashSet[E comparable] map[E]bool

func (s HashSet[E]) Insert(v E)       { s[v] = true }
func (s HashSet[E]) Delete(v E)       { delete(s, v) }
func (s HashSet[E]) Has(v E) bool     { return s[v] }
func (s HashSet[E]) All() iter.Seq[E] { return maps.Keys(s) }

用接口值,而不是復(fù)雜約束,往往更容易讀懂,也更靈活。

總結(jié)

泛型接口是個(gè)很有意思的工具,能幫我們更自然地表達(dá)一些約束關(guān)系。

以下是幾個(gè)要點(diǎn):

  1. 可以用泛型接口來約束元素必須支持“和自己比較”。
  2. 組合 Comparer 和 comparable,能寫出更強(qiáng)大的容器類型。
  3. 接口定義最好保持寬松,把具體約束交給實(shí)現(xiàn)。
  4. 如果因?yàn)橹羔樈邮照吒愠龊軓?fù)雜的泛型函數(shù)簽名,不妨退一步,改用接口值。

泛型給了 Go 更大的表達(dá)能力,但也帶來了復(fù)雜性。

當(dāng)然,我還是建議能用簡單方案解決的,就別上太花哨的寫法。真的容易累人。

責(zé)任編輯:武曉燕 來源: 腦子進(jìn)煎魚了
相關(guān)推薦

2016-03-01 14:51:18

云計(jì)算DevOps

2022-03-22 07:37:04

FeignSpringRibbon

2019-02-20 14:35:57

區(qū)塊鏈數(shù)字貨幣比特幣

2016-01-08 11:00:14

OpenStack云計(jì)算

2023-07-10 09:38:06

兼容性測試方案

2021-11-25 07:43:56

CIOIT董事會(huì)

2017-08-02 10:43:39

深度學(xué)習(xí)TensorFlowRNN

2025-04-30 08:20:58

2021-11-10 16:03:42

Pyecharts Python可視化

2018-10-29 15:20:03

2021-10-09 15:49:00

5G網(wǎng)絡(luò)技術(shù)

2020-07-05 09:17:20

云桌面

2021-06-07 10:05:56

性能優(yōu)化Kafka

2020-06-04 15:16:46

云計(jì)算

2022-06-22 09:06:54

CSS垂直居中代碼

2019-03-17 16:48:51

物聯(lián)網(wǎng)云計(jì)算數(shù)據(jù)信息

2021-01-11 10:47:09

IT部門網(wǎng)絡(luò)管理

2022-08-16 08:33:06

DevOps實(shí)踐

2018-07-03 09:41:23

數(shù)據(jù)庫系統(tǒng) 計(jì)算機(jī)

2021-06-15 11:44:01

芯片
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號