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

          答讀者問(wèn):為什么 Go 里面不能把任意切片轉(zhuǎn)換為 []interface{} ?

          共 4013字,需瀏覽 9分鐘

           ·

          2021-02-02 09:36

          本文來(lái)源于一個(gè)朋友的提問(wèn)。

          數(shù)組怎么樣展開(kāi)?

          問(wèn)題描述比較模糊,進(jìn)一步溝通之后得知,他需要的是將一個(gè)數(shù)組(其實(shí)是切片)展開(kāi),來(lái)作為函數(shù)的可變參數(shù)。

          可變參數(shù)

          關(guān)于可變參數(shù),之前在這里(函數(shù)簽名部分)介紹過(guò)??紤]到那篇文章內(nèi)容比較多,這里再介紹一下。

          簡(jiǎn)單來(lái)說(shuō),可變參數(shù)就是函數(shù)里以 x ...T 這種形式聲明的參數(shù)。舉例說(shuō) f(s ...int),參數(shù) s 接受零到多個(gè) int 型的參數(shù),可以像這樣調(diào)用 f(a, b, c)a,bc 都是 int 型的值)。逐個(gè)傳入的參數(shù)(實(shí)參)會(huì)裝包成一個(gè)切片 s,傳遞給函數(shù)。

          從函數(shù)內(nèi)部的角度,這跟 f(s []int) 是等價(jià)的。而從調(diào)用方的角度看則有差別:可變參數(shù)接受多個(gè) int 型參數(shù),而后者只能接受一個(gè) []int 類(lèi)型參數(shù)。

          如果有多個(gè)同類(lèi)型參數(shù),遇到第二種函數(shù)定義(參數(shù)類(lèi)型是切片),就只能自己先創(chuàng)建一個(gè)切片,再直接傳遞切片。不過(guò)相信你也明白了,可變參數(shù)不過(guò)是把創(chuàng)建切片過(guò)程省略的語(yǔ)法糖

          //?函數(shù)簽名:f(s []int)
          a?:=?[]int{x,?y,?z}
          f(a)

          反過(guò)來(lái),有一個(gè) []int 變量 b ,需要傳遞給可變參數(shù)怎么辦?難道要 f(b[0], b[1], b[2]) 這樣一個(gè)個(gè)用下標(biāo)訪(fǎng)問(wèn)?如果切片很長(zhǎng),又或者直接不確定長(zhǎng)度怎么辦?

          在其它語(yǔ)言,例如 Python 里,對(duì)于可迭代類(lèi)型對(duì)象(Iterator Types),可以用裝包和拆包(解包)解決這個(gè)問(wèn)題,使用上非常靈活。

          Go (看起來(lái))也可以解包:

          //?函數(shù)簽名:f(s ...int)
          f(b...)

          注意 ... 的位置,聲明時(shí)在前,調(diào)用時(shí)在后。

          但,這是一個(gè)假的解包。這只是又一個(gè)語(yǔ)法糖,背后把 b 直接賦值給 s 。把 b 拆分成逐個(gè)參數(shù)傳遞,然后重新打包成切片 s 這件事,根本沒(méi)有發(fā)生。

          你以為的解包:

          (圖中的細(xì)箭頭表示指針,粗箭頭表示拷貝)

          或者至少是這樣的:

          其實(shí)是這樣的:

          切片是引用類(lèi)型,變量本身保存的是頭信息(元數(shù)據(jù)),里面有一個(gè)指向底層數(shù)組的指針,元素?cái)?shù)據(jù)保存在數(shù)組里。在賦值和傳參時(shí),拷貝的只是切片頭(slice header),底層數(shù)組并不會(huì)遞歸拷貝。新舊切片共享同一個(gè)底層數(shù)組。

          ... 只是表示 b 是一組參數(shù),而不是一個(gè)參數(shù)。如果缺少 ... ,直接 f(b) ,會(huì)把 b 直接當(dāng)成一個(gè)參數(shù)(也就是 s 切片的一個(gè)元素),參數(shù)的 []int 類(lèi)型和元素的 int 不匹配。

          好消息是,沒(méi)有額外開(kāi)銷(xiāo)。壞消息是,因此使用上多了很多限制。

          • b 必須是相同類(lèi)型的切片。[]string 傳遞給 []int 固然不行;因?yàn)闆](méi)有經(jīng)過(guò)解包后重新裝包,數(shù)組傳遞給切片也不行。
          • ... (姑且還是叫解包)不能跟其它參數(shù)或者其它解包參數(shù)一起使用。f(x, b...) 或者 f(b..., c...) 都會(huì)報(bào)錯(cuò)。因?yàn)闆](méi)有經(jīng)過(guò)解包后重新裝包,元素 x 和切片 b ,或者bc 兩個(gè)切片,都不會(huì)組成一個(gè)新切片。
          • 修改 s 的元素,會(huì)影響到 b 。(因?yàn)樗鼈児蚕硪粋€(gè)底層數(shù)組)

          類(lèi)型轉(zhuǎn)換

          由于沒(méi)有看到具體代碼,根據(jù)對(duì)方的描述,猜測(cè)問(wèn)題出在沒(méi)有理解『偽解包』上。所以我對(duì)這部分進(jìn)行了解釋。

          然而問(wèn)題并沒(méi)有解決,第二天提問(wèn)者又來(lái)了。

          這次提問(wèn)者給了更詳細(xì)的信息。

          他需要調(diào)用 gorm 包的 Having 方法,方法簽名是:

          func?(s?*DB)?Having(query?interface{},?values?...interface{})?*DB

          看起來(lái)跟我的猜測(cè)差不多。還有什么該注意的我忘了說(shuō)?

          我正想要代碼和具體的報(bào)錯(cuò)信息,對(duì)方說(shuō)了一句:

          為什么 []string 不能轉(zhuǎn)為 []interface{}?

          我一下子明白了問(wèn)題所在:解包的實(shí)參是一個(gè) []string 而不是 []interface{}

          如果是多個(gè) string 變量作為 values 參數(shù),反而沒(méi)有問(wèn)題。但是把 []string 解包,就報(bào)錯(cuò)了。

          當(dāng)然,提問(wèn)者自己也意識(shí)到問(wèn)題出在這里了,只是不明白原因。而我過(guò)分關(guān)注可變參數(shù),忘了留意類(lèi)型。

          這個(gè)現(xiàn)象很容易重現(xiàn),完全沒(méi)必要用到 gorm 包。下面的代碼就報(bào)同樣的錯(cuò)誤:

          package?main

          import?(
          ????"fmt"
          )

          func?main()?{
          ????fmt.Print("this",?1,?"is",?"fine")

          ????ifaces?:=?[]interface{}{1,?"good",?"case",?"here"}
          ????//?OK
          ????fmt.Print(ifaces...)

          ????strs?:=?[]string{"bad",?"case",?"here"}
          ????//?cannot?use?strs?(variable?of?type?[]string)?as?[]interface{}?value?in?argument?to?fmt.Print
          ????fmt.Print(strs...)

          ????ifaces2?:=?make([]interface{},?0)
          ????for?_,?str?:=?range?strs?{
          ????????ifaces2?=?append(ifaces2,?str)
          ????}
          ????//?OK?now
          ????fmt.Print(ifaces2...)
          }

          注意是 fmt.Print(...interface{}) ,內(nèi)置函數(shù) print(...Type) ?的原理不在今天的討論范圍。


          當(dāng)然理解可變參數(shù)也很必要。我們還是需要先理解(偽)解包,知道解包的背后是直接傳遞切片。如果是語(yǔ)言做了真實(shí)的解包和重新裝包,這個(gè)問(wèn)題也就不存在了(見(jiàn) ifaces2 部分代碼)。

          一旦了解這些,提問(wèn)者很自然地發(fā)現(xiàn)問(wèn)題變成了:既然任意類(lèi)型都可以轉(zhuǎn)換為空接口 interface{},為什么 []string (或者任意別的類(lèi)型的切片)不能轉(zhuǎn)為空接口切片 []interface{}

          是的,不可以。其它強(qiáng)類(lèi)型語(yǔ)言也不可以。其它容器也不可以。

          簡(jiǎn)單粗暴的結(jié)論就是:

          • 子類(lèi)型變量可以向父類(lèi)型變量轉(zhuǎn)換;但存放子類(lèi)型的容器跟存放父類(lèi)型的容器沒(méi)有關(guān)系,不能轉(zhuǎn)換。(為了方便理解,父子類(lèi)型借用的 Java 的概念,Go 沒(méi)有繼承機(jī)制。)

          • Go 里面沒(méi)有繼承,只有接口和實(shí)現(xiàn);同時(shí)(暫時(shí))沒(méi)有泛型,只有內(nèi)置派生類(lèi)型(slice, map, chan 等)可以指定元素的類(lèi)型。Go 版本的表述是,即使類(lèi)型 T 滿(mǎn)足接口 I,各自的派生類(lèi)型也沒(méi)有任何關(guān)系(例如 []T[]I)。

          在 Java 里,IntegerNumber 的子類(lèi),ArrayListList 的子類(lèi)。但是,ListList 沒(méi)有繼承關(guān)系,不能轉(zhuǎn)換,只能創(chuàng)建新容器,然后拷貝元素。

          對(duì)應(yīng)到 Go 里,string 滿(mǎn)足 interface{} ,string 變量可以轉(zhuǎn)換為 interface{} 變量;但對(duì)應(yīng)的切片 []string 卻不能轉(zhuǎn)換為 []interface{} 。mapchan 同理。

          原因

          設(shè)計(jì)成這樣的理由,稍微解釋就很容易理解。

          無(wú)論 Java 的類(lèi)繼承和接口實(shí)現(xiàn),還是 Go 的鴨子類(lèi)型接口,都是為了實(shí)現(xiàn)多態(tài)。

          關(guān)于多態(tài)(特別是不同語(yǔ)言下的多態(tài))這里不展開(kāi)。一句話(huà)來(lái)形容的話(huà),Java 的多態(tài)是『代父從軍』,『龍生九子,各有不同』;Go 的多態(tài)則是『如果它跑起來(lái)像鴨子,叫起來(lái)像鴨子,那它就是一只鴨子』,但是每一只『鴨子』可以有自己不同的行為。

          具體的實(shí)現(xiàn)只要滿(mǎn)足相同的約束,就可以賦值給上層抽象類(lèi)型(父類(lèi)型或者接口),當(dāng)作該類(lèi)型使用;與此同時(shí),不同的實(shí)現(xiàn)有不同的行為。調(diào)用代碼只需要認(rèn)準(zhǔn)上層類(lèi)型的約束,不必關(guān)心具體實(shí)現(xiàn)的行為,達(dá)到調(diào)用和實(shí)現(xiàn)的松耦合。這樣可以做到在不修改調(diào)用的情況下,替換掉具體實(shí)現(xiàn)。

          Integer 完全可以當(dāng)作 Number 使用,因?yàn)?Number 有的行為 Integer 都有;日后也可以根據(jù)需要替換成 Float 或者 Double。ArrayListList 也類(lèi)似(注意,T 是同一個(gè)類(lèi)型)。Go 的空接口 interface{} 對(duì)類(lèi)型沒(méi)有任何約束,可以接受任何類(lèi)型。

          可一旦涉及容器,情況就變了。如果一個(gè) ArrayList 可以當(dāng)作 ArrayList ,意味著調(diào)用方可以往里面添加任何 Number 類(lèi)型(及子類(lèi)型),有可能是 Integer ,也可能是 Float 或者 Double 。

          背后的具體實(shí)現(xiàn) ArrayList 可以放別的 Number 類(lèi)型嗎?不行。

          同樣的,[]string 不能存放 string 以外的元素。如果允許 []string 轉(zhuǎn)換成 []interface{} 變量,意味著需要接受任意類(lèi)型的元素

          總結(jié):

          父類(lèi)或者接口作為上層抽象類(lèi)型,在運(yùn)行時(shí)可能會(huì)被替換為任意子類(lèi)型,其可接受的行為應(yīng)該是子類(lèi)型的子集 。(父親會(huì)的技能,孩子們都要會(huì)。父親不能接孩子們不會(huì)的活,否則這個(gè)活就無(wú)法在運(yùn)行時(shí)分派給孩子們干。)

          []interface{} 可以接受的元素類(lèi)型,比任意具體類(lèi)型的切片都要多,顯然不滿(mǎn)足上述條件。從『空接口是任意類(lèi)型的抽象』,得出空接口切片(或者其它容器)也是上層抽象,就屬于想當(dāng)然了。



          推薦閱讀


          福利

          我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù)?ebook?獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 85
          點(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>
                  色久婷婷综合在线亚洲 | 欧美日韩在线观看123 | 黄色毛片电影 | 激情久久成人午夜视频 | 午夜国际熟妇精品影院 |