<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          跟著 Go 作者學(xué)泛型

          共 686字,需瀏覽 2分鐘

           ·

          2022-02-15 09:35

          閱讀本文大概需要 5 分鐘。

          大家好,我是 polarisxu。

          在 GopherCon 2021 年大會(huì)上,Go 兩位作者 Robert Griesemer 和 Ian Lance Taylor 做了泛型相關(guān)的演講,即將在 Go1.18 發(fā)布的 Go 泛型,正是兩位設(shè)計(jì)的。一直想著把他們的演講做一個(gè)梳理,然后分享給大家。拖的有點(diǎn)久,趁春節(jié)假期整理出來(lái)了。

          注意,本文中的 constraints 包,已經(jīng)確定在 Go1.18 正式版中去除,放入 golang.org/x/exp 中。詳細(xì)可以參考該文:Go1.18 這個(gè)包確定沒(méi)了

          Go1.18 關(guān)于泛型部分,主要有三個(gè)特性:

          • Type parameters for functions and types,即函數(shù)和類型的類型參數(shù)
          • Type sets defined by interfaces,即由接口定義的類型集合
          • Type inference,即類型推斷

          1、類型參數(shù)

          先看函數(shù)的類型參數(shù)。

          類型參數(shù)列表(Type parameter lists)

          類型參數(shù)列表看起來(lái)是帶方括號(hào)的普通參數(shù)列表。通常,類型參數(shù)以大寫字母開頭,以強(qiáng)調(diào)它們是類型:

          [P,?Q?constraint1,?R?constraint2]

          看一個(gè)例子。

          非泛型版本的求最小值:

          func?min(x,?y?float64)?float64?{
          ??if?x?????return?x
          ??}
          ??return?y
          }

          如果有 int 類型的 min 版本需求,得另外寫一個(gè)類似的函數(shù),這完全是重復(fù)代碼。

          那泛型版本呢?

          func?min[T?constraints.Ordered](x,?y?T)?T?{
          ??if?x?????return?x
          ??}
          ??return?y
          }

          注意和上面版本的區(qū)別。

          • 多了一個(gè) [T constraints.Ordered],這就是類型參數(shù)列表,聲明了一個(gè)類型 T,它的約束是 constraints.Ordered,即類型 T 滿足它規(guī)定的條件
          • 參數(shù)類型 float64 變成了 T,而不是具體的某個(gè)類型

          那這個(gè)泛型函數(shù)如何調(diào)用呢?

          m?:=?min[int](2,?3)

          是不是很奇怪?其實(shí)仔細(xì)一琢磨,好像沒(méi)問(wèn)題。因?yàn)楹瘮?shù)聲明中有 ?[T constraints.Ordered],跟普通的函數(shù)參數(shù)有點(diǎn)像。調(diào)用時(shí),提供 int,表明普通函數(shù)參數(shù)是 int 類型。

          實(shí)例化

          在調(diào)用時(shí),會(huì)進(jìn)行實(shí)例化過(guò)程:

          1)用類型實(shí)參(type arguments)替換類型形參(type parameters)

          2)檢查類型實(shí)參(type arguments)是否實(shí)現(xiàn)了類型約束

          如果第 2 步失敗,實(shí)例化(調(diào)用)失敗。

          所以,調(diào)用過(guò)程可以分解為以下兩步:

          fmin?:=?min[float64]
          m?:=?fmin(2.3,?3.4)

          //?和下面等價(jià)
          m?:=?min[float64](2.3,?3.4)
          //?相當(dāng)于?m?:=?(min[float64])(2.3,?3.4)

          所以,實(shí)例化產(chǎn)生了一個(gè)非泛型函數(shù)。

          類型的類型參數(shù)

          類型也可以有類型參數(shù)。通過(guò)一個(gè)例子理解一下。

          一個(gè)泛型版二叉樹:

          type?Tree[T?interface{}]?struct?{
          ?left,?right?*Tree[T]
          ?data????????T
          }

          func?(t?*Tree[T])?Lookup(x?T)?*Tree[T]

          var?stringTree?Tree[string]

          注意其中的 [T interface{}] ,跟函數(shù)的類型參數(shù)語(yǔ)法是一樣的,T 相當(dāng)于是一個(gè)類型,所以,之后用到 Tree 的地方,T 都跟隨著,即 Tree[T],包括方法的接收者(receiver)。

          注意實(shí)例化的地方:var stringTree Tree[string],和上面兩個(gè)實(shí)例化步驟中的第一步一樣。

          2、類型集合(Type sets)

          先看值參數(shù)的類型(the type of value parameters)。

          函數(shù)普通參數(shù)列表中的每個(gè)值參數(shù)都有一個(gè)類型,這個(gè)類型定義值的集合。比如 float64 定義了浮點(diǎn)數(shù)值的集合。

          相應(yīng)的有類型參數(shù)的類型(the type of type parameters),也就是說(shuō),類型參數(shù)列表中的每個(gè)類型參數(shù)都有一個(gè)類型,這個(gè)類型定義了類型的集合,這叫做類型約束(type constraint):

          func?min[T?constraints.Ordered](x,?y?T)?T

          這里的 constraints.Ordered 是類型參數(shù)列表中的 T 參數(shù)的類型,它定義了類型的集合,即類型約束。

          constraints.Ordered 是 Go1.18 內(nèi)置的一個(gè)類型約束,它有兩個(gè)功能:

          • 只有值支持排序的類型才能傳遞給類型參數(shù) T;
          • T 類型的值必須支持 < 操作符,因?yàn)楹瘮?shù)體中使用了該操作符。

          類型約束是接口

          大家都知道接口定義了方法集(method sets),演講中給了一張圖:

          method sets

          根據(jù) Go 的規(guī)則,類型 P、Q、R 方法中包含了 a、b、c,因此它們實(shí)現(xiàn)了接口。

          所以,反過(guò)來(lái)可以說(shuō),接口也定義了類型集(type sets):

          type sets

          上圖中,類型 P、Q、R 都實(shí)現(xiàn)了左邊的接口(因?yàn)槎紝?shí)現(xiàn)了接口的方法集),因此我們可以說(shuō)該接口定義了類型集。

          既然接口是定義類型集,只不過(guò)是間接定義的:類型實(shí)現(xiàn)接口的方法集。而類型約束是類型集,因此完全可以重用接口的語(yǔ)義,只不過(guò)這次是直接定義類型集:

          interface defines type sets

          這就是類型約束的語(yǔ)法,通過(guò)接口直接定義類型集:

          type?Xxx?interface?{
          ??int?|?string?|?bool
          }

          而 constraints.Ordered 的定義如下:

          //?Ordered?is?a?constraint?that?permits?any?ordered?type:?any?type
          //?that?supports?the?operators?=?>.
          //?If?future?releases?of?Go?add?new?ordered?types,
          //?this?constraint?will?be?modified?to?include?them.
          type?Ordered?interface?{
          ?Integer?|?Float?|?~string
          }

          Ordered 定義了所有 interger、浮點(diǎn)數(shù)和字符串類型的集合。所以,< 操作符也是支持的。這其中的 Integer、Float 也在 constraints 包有定義。

          細(xì)心的朋友應(yīng)該發(fā)現(xiàn)了 ~string,類型前面的 ~~T 意味著包含底層類型 T 的所有類型集合。

          如果約束中的所有類型都支持一個(gè)操作,則該操作可以與相應(yīng)的類型參數(shù)一起使用

          除了將約束單獨(dú)定義為類型外,還可以寫成字面值的形式,比如:

          [S?interface{~[]E},?E?interface{}]

          這看著有點(diǎn)暈,其實(shí)可以直接這么寫:

          [S?~[]E,?E?interface{}]

          Go1.18 中,any 是 interface{} 的別名,因此可以進(jìn)一步寫為:

          [S?~[]E,?E?any]

          E 是切片的元素類型,~[]E 表示底層是 []E 切片類型的都符合該約束。

          3、類型推斷

          在調(diào)用泛型函數(shù)時(shí),提供類型實(shí)參感覺有點(diǎn)多余。Go 雖然是靜態(tài)類型語(yǔ)言,但擅長(zhǎng)類型推斷。因此泛型這里,Go 也實(shí)現(xiàn)了類型推斷。

          調(diào)用泛型版的 min,可以不提供類型實(shí)參,而是直接由 Go 進(jìn)行類型推斷:

          var?a,?b,?m?float64
          m?:=?min[float64](a,?b)

          類型推斷的細(xì)節(jié)很復(fù)雜,但使用起來(lái)還是很簡(jiǎn)單,大部分時(shí)候,跟普通函數(shù)調(diào)用沒(méi)有區(qū)別。

          關(guān)于類型推斷,演講中給了一個(gè)例子:

          func?Scale[E?constraints.Integer](s?[]E,?c?E)?[]E?{
          ?r?:=?make([]E,?len(s))
          ?for?i,?v?:=?range?s?{
          ??r[i]?=?v?*?c
          ?}
          ?return?r
          }

          這個(gè)函數(shù)的目的是希望對(duì) s 中的每個(gè)元素都乘以參數(shù) c,最后返回一個(gè)新的切片。

          接著定義一個(gè)類型:

          type?Point?[]int32

          func?(p?Point)?String()?string?{
          ?//?實(shí)現(xiàn)細(xì)節(jié)不重要,忽略
          ?return?"point"
          }

          很顯然,Point 類型的切片可以傳遞給 Scale:

          func?ScaleAndPrint(p?Point)?{
          ?r?:=?Scale(p,?2)
          ?fmt.Println(r.String())
          }

          我們希望對(duì) p 進(jìn)行 Scale,得到一個(gè)新的 p,但發(fā)現(xiàn)返回的 r 根本不是 Point:

          func?main()?{
          ?p?:=?Point{3,?2,?4}
          ?ScaleAndPrint(p)
          }

          會(huì)報(bào)錯(cuò):r.String undefined (type []int32 has no field or method String)

          所以,我們應(yīng)該這樣修改 Scale 函數(shù):

          func?Scale[S?~[]E,?E?constraints.Integer](s?S,?c?E)?S?{
          ?r?:=?make(S,?len(s))
          ?for?i,?v?:=?range?s?{
          ??r[i]?=?v?*?c
          ?}
          ?return?r
          }

          注意其中的變化:加入了泛型 S,以及額外的類型約束 ~[]E

          調(diào)用 Scale 時(shí),不需要 r := Scale[Point, int32](p, 2),因?yàn)?Go 會(huì)進(jìn)行類型推斷。

          正確的完整代碼如下:

          package?main

          import?(
          ?"constraints"
          ?"fmt"
          )

          func?Scale[S?~[]E,?E?constraints.Integer](s?S,?c?E)?S?{
          ?r?:=?make(S,?len(s))
          ?for?i,?v?:=?range?s?{
          ??r[i]?=?v?*?c
          ?}
          ?return?r
          }

          type?Point?[]int32

          func?(p?Point)?String()?string?{
          ?//?實(shí)現(xiàn)細(xì)節(jié)不重要,忽略
          ?return?"point"
          }

          func?ScaleAndPrint(p?Point)?{
          ?r?:=?Scale(p,?2)
          ?fmt.Println(r.String())
          }

          func?main()?{
          ?p?:=?Point{3,?2,?4}
          ?ScaleAndPrint(p)
          }

          4、什么時(shí)候用泛型

          泛型的加入,無(wú)疑增加了復(fù)雜度。我個(gè)人認(rèn)為,能不用泛型就不用泛型。在演講中,兩位大佬提到,在以下場(chǎng)景可以考慮使用泛型:

          • 對(duì)于 slice、map、channel 等類型,如果它們的元素類型是不確定的,操作這類類型的函數(shù)可以考慮用泛型
          • 一些通用目的的數(shù)據(jù)結(jié)構(gòu),比如前面提到的二叉樹等
          • 如果一些函數(shù)行為相同,只是類型不同,可以考慮用泛型重構(gòu)

          注意,目前 Go 方法不支持類型參數(shù),所以,如果方法有需要泛型的場(chǎng)景,可以轉(zhuǎn)為函數(shù)的形式。

          此外,不要為了泛型而泛型。比如這樣的泛型就很糟糕:

          func?ReadFour[T?io.Reader](r?T)?([]byte,?error)

          而應(yīng)該使用非泛型版本:

          func?ReadFour(r?io.Reader)?([]byte,?error)

          5、總結(jié)

          泛型是一把雙刃劍。泛型的加入,讓 Go 不那么簡(jiǎn)單了。有些代碼寫出來(lái),可讀性可能非常差。我們應(yīng)該按沒(méi)有泛型時(shí)候?qū)懘a,當(dāng)發(fā)現(xiàn)在 Repeat Yourself 時(shí),再考慮能不能用泛型重構(gòu),千萬(wàn)別玩什么花樣!

          最后,放上演講的視頻地址,有興趣的可以觀看:https://www.youtube.com/watch?v=Pa_e9EeCdy8。




          往期推薦


          我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語(yǔ)言并創(chuàng)建了 Go 語(yǔ)言中文網(wǎng)!著有《Go語(yǔ)言編程之旅》、開源圖書《Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)》等。


          堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場(chǎng)心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長(zhǎng)!也歡迎加我微信好友交流:gopherstudio

          瀏覽 65
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐91视频 | 北条麻妃91在线播放 | 九色PORNY国产成人 | 男女练啪在线观看视频 | 无码人在线观 |