<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 語言:全面分析為什么我們需要泛型

          共 8607字,需瀏覽 18分鐘

           ·

          2020-12-01 16:42

          點(diǎn)擊上方藍(lán)色“Go語言中文網(wǎng)”關(guān)注,每天一起學(xué) Go

          從 golang 誕生起是否應(yīng)該添加泛型支持就是一個熱度未曾消減的議題。泛型的支持者們認(rèn)為沒有泛型的語言是不完整的,而泛型的反對者們則認(rèn)為接口足以取代泛型,增加泛型只會徒增語言的復(fù)雜度。雙方各執(zhí)己見,爭執(zhí)不下,直到官方最終確定泛型是 go2 的發(fā)展路線中的重中之重。

          今天我們就來看看為什么我們需要泛型,沒有泛型時我們在做什么,泛型會帶來哪些影響,泛型能拯救我們嗎?

          本文索引

          • 沒有泛型的世界
            • 暴力窮舉
            • 依靠通用引用類型
            • 動態(tài)類型語言的特例
          • 動靜結(jié)合
            • 使用 interface 模擬泛型
            • interface 會進(jìn)行嚴(yán)格的類型檢查
            • 內(nèi)置類型何去何從
            • 性能陷阱
            • 復(fù)合類型的迷思
            • 最后也是最重要的
          • 泛型帶來的影響,以及拯救
            • 徹底從沒有泛型的泥沼中解放
            • 泛型的代價

          沒有泛型的世界

          泛型最常見也是最簡單的需求就是創(chuàng)建一組操作相同或類似的算法,這些算法應(yīng)該是和數(shù)據(jù)類型無關(guān)的,不管什么數(shù)據(jù)類型只要符合要求就可以操作。

          看起來很簡單,我們只需要專注于算法自身的實(shí)現(xiàn),而不用操心其他細(xì)枝末節(jié)。然而現(xiàn)實(shí)是骨感的,想要實(shí)現(xiàn)類型無關(guān)算法在沒有泛型的世界里卻是困難的,需要在許多條件中利弊取舍。

          下面我們就來看看在沒有泛型的參與下我們是如何處理數(shù)據(jù)的。

          暴力窮舉

          這是最簡單也是最容易想到的方法。

          既然算法部分的代碼是幾乎相同的,那么就 copy 幾遍,然后把數(shù)據(jù)類型的地方做個修改替換,這樣的工作甚至可以用文本編輯器的代碼片段+查找替換來快速實(shí)現(xiàn)。比如下面的 c 代碼:

          float?a?=?logf(2.0f);
          double?b?=?log(2.0);

          typedef?struct?{
          ????int?*data;
          ????unsigned?int?max_size;
          }?IntQueue;

          typedef?struct?{
          ????double?*data;
          ????unsigned?int?max_size;
          }?DoubleQueue;

          IntQueue*?NewIntQueue(unsigned?int?size)
          {
          ????IntQueue*?q?=?(IntQueue*)malloc(sizeof(IntQueue));
          ????if?(q?==?NULL)?{
          ????????return?NULL;
          ????}
          ????q->max_size?=?size;
          ????q->data?=?(int*)malloc(size?*?sizeof(int));
          ????return?q;
          }

          DoubleQueue*?NewDoubleQueue(unsigned?int?size)
          {
          ????DoubleQueue*?q?=?(DoubleQueue*)malloc(sizeof(DoubleQueue));
          ????if?(q?==?NULL)?{
          ????????return?NULL;
          ????}
          ????q->max_size?=?size;
          ????q->data?=?(double*)malloc(size?*?sizeof(double));
          ????return?q;
          }

          問題看上去解決了,除了修改和復(fù)查比較麻煩之外。做程序員的誰還沒有 cv 過呢,然而這種方法缺點(diǎn)很明顯:

          • 嚴(yán)重違反 DRY(don't repeat yourself),數(shù)據(jù)結(jié)構(gòu)的修改和擴(kuò)展極其困難
          • 復(fù)制粘貼修改中可能會出現(xiàn)低級的人力錯誤,并且耗費(fèi)精力
          • 最關(guān)鍵的一點(diǎn),我們不可能針對所有類型去寫出特定的算法,因?yàn)檫@些類型的數(shù)量少則 5,6 種,多則上不封頂。

          當(dāng)然,好處也不是沒有:

          • 保證了類型安全,任何類型問題都能在編譯期暴露
          • 更靈活,對于某些特定類型我們還可以做出非常細(xì)致的優(yōu)化工作(比如對于 bool 類型我們可以使用 unsigned int 這個一般來說 4 字節(jié)大小的類型存放 32 個 bool 值,而不是用 32 個 bool 變量消耗 32 字節(jié)內(nèi)存)

          然而缺點(diǎn) 1 和缺點(diǎn) 3 給予的是致命打擊,因此通常我們不會用這種方法實(shí)現(xiàn)通用算法和數(shù)據(jù)結(jié)構(gòu)。(然而不幸的是 golang 中的 math/rand 就是這么實(shí)現(xiàn)的)

          依靠通用引用類型

          其實(shí)方案 1 還可以依靠宏來實(shí)現(xiàn),linux 內(nèi)核就是這么做的,不過宏這個機(jī)制不是每個語言都有的,因此參考價值不是很高。

          既然明確指出數(shù)據(jù)的類型不可行,那我們還有其他的辦法。比如馬上要介紹的使用通用類型引用數(shù)據(jù)。

          通用的引用類型,表示它可以引用其他不同類型的數(shù)據(jù)而自身的數(shù)據(jù)類型不會改變,比如 c 中的void *

          void?*ptr?=?NULL;
          ptr?=?(void*)"hello";
          int?a?=?100;
          ptr?=?(void*)&a;

          c 語言允許非函數(shù)指針的數(shù)據(jù)類型指針轉(zhuǎn)換為void *,因此我們可以用它來囊括幾乎所有的數(shù)據(jù)(函數(shù)除外)。

          于是 Queue 的代碼就會變成如下的畫風(fēng):

          typedef?struct?{
          ????void?*data;
          ????unsigned?int?max_size;
          }?Queue;

          Queue*?NewQueue(unsigned?int?size)
          {
          ????Queue*?q?=?(Queue*)malloc(sizeof(Queue));
          ????if?(q?==?NULL)?{
          ????????return?NULL;
          ????}
          ????q->max_size?=?size;
          ????q->data?=?//?這里填什么呢?
          }

          代碼寫了一半發(fā)現(xiàn)寫不下去了?放心,這不是你的問題。在 c 語言里我們不能創(chuàng)建void類型的變量,所以我們不可能給 data 預(yù)先分配內(nèi)存。

          那么退一步考慮,如果引入一個 java 那樣的類似void*Object類型,是否就能解決內(nèi)存分配呢?答案是否定的,假設(shè) Object 大小是 8 字節(jié),如果我們放一個通常只有一字節(jié)大小的 bool 進(jìn)去就會有 7 字節(jié)的浪費(fèi),如果我們放一個 32 字節(jié)的自定義類型,那么很顯然一個 Object 的空間是遠(yuǎn)遠(yuǎn)不夠的。在 c 這樣的語言中我們想要使用數(shù)據(jù)就需要知道該數(shù)據(jù)的類型,想要確定類型就要先確定它的內(nèi)存布局,而要能確定內(nèi)存布局第一步就是要知道類型需要的內(nèi)存空間大小。

          遺憾的是通用引用類型幫我們把具體的類型信息全部擦除了。

          寫程序最重要的就是發(fā)散型的思維,如果你看到這里覺得本方案不行了的話你就太天真了。別的不說,java 能用 Object 實(shí)現(xiàn)泛用容器,c 也可以。秘訣很簡單,既然我們不能準(zhǔn)確創(chuàng)建類型的實(shí)例,那不創(chuàng)建不就行了嘛。隊(duì)列本來就是負(fù)責(zé)存取數(shù)據(jù)的,創(chuàng)建這種工作外包給其他代碼就行了:

          typedef?struct?{
          ????unsigned?int?max_size;
          ????unsigned?int?current;
          ????void?**data;
          }?Queue;

          Queue*?NewQueue(unsigned?int?size)
          {
          ????Queue*?q?=?(Queue*)malloc(sizeof(Queue));
          ????if?(q?==?NULL)?{
          ????????return?NULL;
          ????}
          ????q->max_size?=?size;
          ????q->size?=?0;
          ????q->data?=?(void?**)malloc(size*sizeof(void*));
          }

          bool?QueuePush(Queue*?q,?void*?value)
          {
          ????if?(q?==?NULL?||?value?==?NULL?||?q->current?==?q->max_size-1)?{
          ????????return?false;
          ????}

          ????q->data[q->current++]?=?value;
          ????return?true;
          }

          It works! 但是我們需要隊(duì)列中的類型有特定操作呢?把操作抽象形成函數(shù)再傳遞給隊(duì)列的方法就行了,可以參考 c 的 qsort 和 bsearch:

          #include?

          void?qsort(void?*base,?size_t?nmemb,?size_t?size,
          ??????????????????int?(*compar)(const?void?*,?const?void?*))
          ;

          void?*bsearch(const?void?*key,?const?void?*base,
          ?????????????????????size_t?nmemb,?size_t?size,
          ?????????????????????int?(*compar)(const?void?*,?const?void?*))
          ;

          更普遍的,你可以用鏈表去實(shí)現(xiàn)隊(duì)列:

          typedef?struct?node?{
          ???int?val;
          ???struct?node?*next;
          }?node_t;

          void?enqueue(node_t?**head,?int?val)?{
          ???node_t?*new_node?=?malloc(sizeof(node_t));
          ???if?(!new_node)?return;

          ???new_node->val?=?val;
          ???new_node->next?=?*head;

          ???*head?=?new_node;
          }

          原理同樣是將創(chuàng)建具體的數(shù)據(jù)的任務(wù)外包,只不過鏈表額外增加了一層 node 的包裝罷了。

          那么這么做的好處和壞處是什么呢?

          好處是我們可以遵守 DRY 原則了,同時還能專注于隊(duì)列本身的實(shí)現(xiàn)。

          壞處那就有點(diǎn)多了:

          • 首先是類型擦除的同時沒有任何類型檢測的手段,因此類型安全無從保證,比如存進(jìn)去的可以是int,取出來的時候你可以轉(zhuǎn)換成char*,程序不會給出任何警告,等你準(zhǔn)備從這個char*里取出某個位置上的字符的時候就會引發(fā)未定義行為,從而出現(xiàn)許許多多奇形怪狀的 bug
          • 只能存指針類型
          • 如何確定隊(duì)列里存儲數(shù)據(jù)的所有權(quán)?交給隊(duì)列管理會增加隊(duì)列實(shí)現(xiàn)的復(fù)雜性,不交給隊(duì)列管理就需要手動追蹤 N 個對象的生命周期,心智負(fù)擔(dān)很沉重,并且如果我們是存入的局部變量的指針,那么交給隊(duì)列管理就一定會導(dǎo)致 free 出現(xiàn)未定義行為,從代碼層面我們是幾乎不能區(qū)分一個指針是不是真的指向了堆上的內(nèi)容的
          • 依舊不能避免書寫類型代碼,首先使用數(shù)據(jù)時要從void*轉(zhuǎn)換為對應(yīng)類型,其次我們需要書寫如 qsort 例子里那樣的幫助函數(shù)。

          動態(tài)類型語言的特例

          在真正進(jìn)入本節(jié)的主題之前,我想先介紹下什么是動態(tài)類型,什么是靜態(tài)類型。

          所謂靜態(tài)類型,就是在編譯期能夠確定的變量、表達(dá)式的數(shù)據(jù)類型,換而言之,編譯期如果就能確定某個類型的內(nèi)存布局,那么它就是靜態(tài)類型。舉個 c 語言的例子:

          int?a?=?0;
          const?char?*str?=?"hello?generic";
          double?values[]?=?{1.,?2.,?3.};

          上述代碼中intconst char *、double[3]都是靜態(tài)類型,其中intconst char *(指針類型不受底層類型的影響,大家有著相同的大?。?biāo)準(zhǔn)中都給出了類型所需的最小內(nèi)存大小,而數(shù)組類型是帶有長度的,或者是在表達(dá)式和參數(shù)傳遞中退化(decay)為指針類型,因此編譯器在編譯這些代碼的時候就能知道變量所需的內(nèi)存大小,進(jìn)而確定了其在內(nèi)存中的布局。當(dāng)然靜態(tài)類型其中還有許多細(xì)節(jié),這里暫時不必深究。

          回過來看動態(tài)類型就很好理解了,編譯期間無法確定某個變量、表達(dá)式的具體類型,這種類型就是動態(tài)的,例如下面的 python 代碼:

          name?=?'apocelipes'
          name?=?12345

          name 究竟是什么類型的變量?不知道,因?yàn)?name 實(shí)際上可以賦值任意的數(shù)據(jù),我們只能在運(yùn)行時的某個點(diǎn)做類型檢測,然后斷言 name 是 xxx 類型的,然而過了這個時間點(diǎn)之后 name 還可以賦值一個完全不同類型的數(shù)據(jù)。

          好了現(xiàn)在我們回到正題,可能你已經(jīng)猜到了,我要說的特例是什么。沒錯,因?yàn)閯討B(tài)類型語言實(shí)際上不關(guān)心數(shù)據(jù)的具體類型是什么,所以即使沒有泛型你也可以寫出類似泛型的代碼,而且通常它們工作得很好:

          class?Queue:
          ????def?__init__(self):
          ????????self.data?=?[]

          ????def?push(self,?value):
          ????????self.data.append()

          ????def?pop(self):
          ????????self.data.pop()

          ????def?take(self,?index):
          ????????return?self.data[index]

          我們既能放字符串進(jìn)Queue也能放整數(shù)和浮點(diǎn)數(shù)進(jìn)去。然而這并不能稱之為泛型,使用泛型除了因?yàn)榭梢陨賹懼貜?fù)的代碼,更重要的一點(diǎn)是可以確保代碼的類型安全,看如下例子,我們給 Queue 添加一個方法:

          def?transform(self):
          ????for?i?in?range(len(self.data)):
          ????????self.data[i]?=?self.data[i].upper()

          我們提供了一個方法,可以將隊(duì)列中的字符串從小寫轉(zhuǎn)換為大寫。問題發(fā)生了,我們的隊(duì)列不僅可以接受字符串,它還可以接受數(shù)字,這時候如果我們調(diào)用transform方法就會發(fā)生運(yùn)行時異常:AttributeError: 'int' object has no attribute 'upper'。那么怎么避免問題呢?添加運(yùn)行時的類型檢測就可以了,然而這樣做有兩個無法繞開的弊端:

          • 寫出了類型相關(guān)的代碼,和我們本意上想要實(shí)現(xiàn)類型無關(guān)的代碼結(jié)構(gòu)相沖突
          • 限定了算法只能由幾種數(shù)據(jù)類型使用,但事實(shí)上有無限多的類型可以實(shí)現(xiàn) upper 方法,然而我們不能在類型檢查里一一列舉他們,從而導(dǎo)致了我們的通用算法變?yōu)榱讼薅ㄋ惴ā?/section>

          動靜結(jié)合

          沒有泛型的世界實(shí)在是充滿了煎熬,不是在違反 DRY 原則的邊緣反復(fù)試探,就是冒著類型安全的風(fēng)險激流勇進(jìn)。有什么能脫離苦海的辦法嗎?

          作為一門靜態(tài)強(qiáng)類型語言,golang 提供了一個不是太完美的答案——interface。

          使用 interface 模擬泛型

          interface 可以接受任何滿足要求的類型的數(shù)據(jù),并且具有運(yùn)行時的類型檢查。雙保險很大程度上提升了代碼的安全性。

          一個典型的例子就是標(biāo)準(zhǔn)庫里的 containers:

          package?list?//?import?"container/list"

          Package?list?implements?a?doubly?linked?list.

          To?iterate?over?a?list?(where?l?is?a?*List):

          ????for?e?:=?l.Front();?e?!=?nil;?e?=?e.Next()?{
          ????????//?do?something?with?e.Value
          ????}

          type?Element?struct{?...?}
          type?List?struct{?...?}
          ????func?New()?*List

          type?Element?struct?{

          ????????//?The?value?stored?with?this?element.
          ????????Value?interface{}
          ????????//?Has?unexported?fields.
          }
          ????Element?is?an?element?of?a?linked?list.

          func?(e?*Element)?Next()?*Element
          func?(e?*Element)?Prev()?*Element

          type?List?struct?{
          ????????//?Has?unexported?fields.
          }
          ????List?represents?a?doubly?linked?list.?The?zero?value?for?List?is?an?empty
          ????list?ready?to?use.

          func?New()?*List
          func?(l?*List)?Back()?*Element
          func?(l?*List)?Front()?*Element
          func?(l?*List)?Init()?*List
          func?(l?*List)?InsertAfter(v?interface{},?mark?*Element)?*Element
          func?(l?*List)?InsertBefore(v?interface{},?mark?*Element)?*Element
          func?(l?*List)?Len()?int
          func?(l?*List)?MoveAfter(e,?mark?*Element)
          func?(l?*List)?MoveBefore(e,?mark?*Element)
          func?(l?*List)?MoveToBack(e?*Element)
          func?(l?*List)?MoveToFront(e?*Element)
          func?(l?*List)?PushBack(v?interface{})?*Element
          func?(l?*List)?PushBackList(other?*List)
          ...

          這就是在上一大節(jié)中的方案 2 的類型安全強(qiáng)化版。接口的工作原理本文不會詳述。

          但事情遠(yuǎn)沒有結(jié)束,假設(shè)我們要對一個數(shù)組實(shí)現(xiàn) indexOf 的通用算法呢?你的第一反應(yīng)大概是下面這段代碼:

          func?IndexOfInterface(arr?[]interface{},?value?interface{})?int?{
          ?for?i,?v?:=?range?arr?{
          ??if?v?==?value?{
          ???return?i
          ??}
          ?}

          ?return?-1
          }

          這里你會接觸到 interface 的第一個坑。

          interface 會進(jìn)行嚴(yán)格的類型檢查

          看看下面代碼的輸出,你能解釋為什么嗎?

          func?ExampleIndexOfInterface()?{
          ????arr?:=?[]interface{}{uint(1),uint(2),uint(3),uint(4),uint(5)}
          ?fmt.Println(IndexOfInterface(arr,?5))
          ????fmt.Println(IndexOfInterface(arr,?uint(5)))
          ????//?Output:
          ????//?-1
          ????//?4
          }

          會出現(xiàn)這種結(jié)果是因?yàn)?interface 的相等需要類型和值都相等,字面量 5 的值是 int,所以沒有搜索到相等的值。

          想要避免這種情況也不難,創(chuàng)建一個Comparable接口即可:

          type?Comparator?interface?{
          ?Compare(v?interface{})?bool
          }

          func?IndexOfComparator(arr?[]Comparator,?value?Comparator)?int?{
          ?for?i,v?:=?range?arr?{
          ??if?v.Compare(value)?{
          ???return?i
          ??}
          ?}
          ?return?-1
          }

          這回我們不會出錯了,因?yàn)樽置媪扛静荒軅魅牒瘮?shù),因?yàn)閮?nèi)置類型都沒實(shí)現(xiàn)Comparator接口。

          內(nèi)置類型何去何從

          然而這是接口的第二個坑,我們不得不為內(nèi)置類型創(chuàng)建包裝類和包裝方法。

          假設(shè)我們還想把前文的arr直接傳入IndexOfComparator,那必定得到編譯器的抱怨:

          cannot?use?arr?(type?[]interface?{})?as?type?[]Comparator?in?argument?to?IndexOfComparator

          為了使用這個函數(shù)我們不得不對代碼進(jìn)行修改:

          type?MyUint?uint

          func?(u?MyUint)?Compare(v?interface{})?bool?{
          ?value?:=?v.(MyUint)
          ?return?u?==?value
          }

          arr2?:=?[]Comparator{MyUint(1),MyUint(2),MyUint(3),MyUint(4),MyUint(5)}
          fmt.Println(IndexOfComparator(arr2,?MyUint(5)))

          我們希望泛型能簡化代碼,但現(xiàn)在卻反其道而行之了。

          性能陷阱

          第三個,也是被人詬病最多的,是接口帶來的性能下降。

          我們對如下幾個函數(shù)做個簡單的性能測試:

          func?IndexOfByReflect(arr?interface{},?value?interface{})?int?{
          ?arrValue?:=?reflect.ValueOf(arr)
          ?length?:=?arrValue.Len()
          ?for?i?:=?0;?i???if?arrValue.Index(i).Interface()?==?value?{
          ???return?i
          ??}
          ?}
          ?return?-1
          }

          func?IndexOfInterface(arr?[]interface{},?value?interface{})?int?{
          ?for?i,?v?:=?range?arr?{
          ??if?v?==?value?{
          ???return?i
          ??}
          ?}

          ?return?-1
          }

          func?IndexOfInterfacePacking(value?interface{},?arr?...interface{})?int?{
          ?for?i,?v?:=?range?arr?{
          ??if?v?==?value?{
          ???return?i
          ??}
          ?}

          ?return?-1
          }

          這是測試代碼(golang1.15.2):

          const?ArrLength?=?500
          var?_arr?[]interface{}
          var?_uintArr?[]uint

          func?init()?{
          ?_arr?=?make([]interface{},?ArrLength)
          ?_uintArr?=?make([]uint,?ArrLength)
          ?for?i?:=?0;?i?1;?i++?{
          ??_uintArr[i]?=?uint(rand.Int()?%?10?+?2)
          ??_arr[i]?=?_uintArr[i]
          ?}
          ?_arr[ArrLength?-?1]?=?uint(1)
          ?_uintArr[ArrLength?-?1]?=?uint(1)
          }

          func?BenchmarkIndexOfInterface(b?*testing.B)?{
          ?for?i?:=?0;?i???IndexOfInterface(_arr,?uint(1))
          ?}
          }

          func?BenchmarkIndexOfInterfacePacking(b?*testing.B)?{
          ?for?i?:=?0;?i???IndexOfInterfacePacking(uint(1),?_arr...)
          ?}
          }

          func?indexOfUint(arr?[]uint,?value?uint)?int?{
          ?for?i,v?:=?range?arr?{
          ??if?v?==?value?{
          ???return?i
          ??}
          ?}

          ?return?-1
          }

          func?BenchmarkIndexOfUint(b?*testing.B)?{
          ?for?i?:=?0;?i???indexOfUint(_uintArr,?uint(1))
          ?}
          }

          func?BenchmarkIndexOfByReflectInterface(b?*testing.B)?{
          ?for?i?:=?0;?i???IndexOfByReflect(_arr,?uint(1))
          ?}
          }

          func?BenchmarkIndexOfByReflectUint(b?*testing.B)?{
          ?for?i?:=?0;?i???IndexOfByReflect(_uintArr,?uint(1))
          ?}
          }
          img

          我們吃驚地發(fā)現(xiàn),直接使用 interface 比原生類型慢了 10 倍,如果使用反射并接收原生將會慢整整 100 倍!

          另一個使用接口的例子是比較 slice 是否相等,我們沒有辦法直接進(jìn)行比較,需要借助輔助手段,在我以前的這篇博客有詳細(xì)的講解。性能問題同樣很顯眼。

          復(fù)合類型的迷思

          interface{}是接口,而[]interface{}只是一個普通的 slice。復(fù)合類型中的接口是不存在協(xié)變的。所以下面的代碼是有問題的:

          func?work(arr?[]interface{})?{}

          ss?:=?[]string{"hello",?"golang"}
          work(ss)

          類似的問題其實(shí)在前文里已經(jīng)出現(xiàn)過了。這導(dǎo)致我們無法用 interface 統(tǒng)一處理 slice,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">interface{}并不是 slice,slice 的操作無法對 interface 使用。

          為了解決這個問題,golang 的 sort 包給出了一個頗為曲折的方案:

          img

          sort 為了能處理 slice,不得不包裝了常見的基本類型的 slice,為了兼容自定義類型包里提供了Interface,需要你自己對自定義類型的 slice 進(jìn)行包裝。

          這實(shí)現(xiàn)就像是千層餅,一環(huán)套一環(huán),即使內(nèi)部的 quicksort 寫得再漂亮性能也是要打不少折扣的。

          最后也是最重要的

          對于獲取接口類型變量的值,我們需要類型斷言,然而類型斷言是運(yùn)行時進(jìn)行的:

          var?i?interface{}
          i?=?1
          s?:=?i.(string)

          這會導(dǎo)致 panic。如果不想 panic 就需要第二個變量去獲取是否類型斷言成功:s, ok := i.(string)。

          然而真正的泛型是在編譯期就能發(fā)現(xiàn)這類錯誤的,而不是等到程序運(yùn)行得如火如荼時突然因?yàn)?panic 退出。

          泛型帶來的影響,以及拯救

          徹底從沒有泛型的泥沼中解放

          同樣是上面的 IndexOf 的例子,有了泛型我們可以簡單寫為:

          package?main

          import?(
          ?"fmt"
          )

          func?IndexOf[T?comparable](arr?[]T,?value?T)?int?{
          ????for?i,?v?:=?range?arr?{
          ????????if?v?==?value?{
          ????????????return?i
          ????????}
          ????}

          ????return?-1
          }

          func?main()?{
          ?q?:=?[]uint{1,2,3,4,5}
          ?fmt.Println(IndexOf(q,?5))
          }

          comparable是 go2 提供的內(nèi)置設(shè)施,代表所有可比較類型,你可以在這里運(yùn)行上面的測試代碼。

          泛型函數(shù)會自動做類型推導(dǎo),字面量可以用于初始化 uint 類型,所以函數(shù)正常運(yùn)行。

          代碼簡單干凈,而且沒有性能問題(至少官方承諾泛型的絕大部分工作會在編譯期完成)。

          再舉個 slice 判斷相等的例子:

          func?isEqual[T?comparable](a,b?[]T)?bool?{
          ????if?len(a)?!=?len(b)?{
          ????????return?false;
          ????}

          ????for?i?:=?range?a?{
          ????????if?a[i]?!=?b[i]?{
          ????????????return?false
          ????????}
          ????}

          ????return?true
          }

          除了大幅簡化代碼之外,泛型還將給我們帶來如下改變:

          • 真正的類型安全,像isEqual([]int, []string)這樣的代碼在編譯時就會被發(fā)現(xiàn)并被我們修正
          • 雖然泛型也不支持協(xié)變,但 slice 等復(fù)合類型只要符合參數(shù)推導(dǎo)的規(guī)則就能被使用,限制更少
          • 沒有了接口和反射,性能自不必說,編譯期就能確定變量類型的話還可以增加代碼被優(yōu)化的機(jī)會

          可以說泛型是真正救人于水火。這也是泛型最終能進(jìn)入 go2 提案的原因。

          泛型的代價

          最后說了這么多泛型的必要性,也該是時候談?wù)劮盒椭盗恕?/p>

          其實(shí)目前 golang 的泛型還在提案階段,雖然已經(jīng)有了預(yù)覽版,但今后變數(shù)還是很多,所以這里只能針對草案簡單說說兩方面的問題。

          第一個還是類型系統(tǒng)的割裂問題,golang 使用的泛型系統(tǒng)比 typwscript 更加嚴(yán)格,any 約束的類型甚至無法使用賦值運(yùn)算之外的其他內(nèi)置運(yùn)算符。因此想要類型能比較大小的時候必定創(chuàng)建自定義類型和自定義的類型約束,內(nèi)置類型是無法添加方法的,所以需要包裝類型。

          解決這個問題不難,一條路是 golang 官方提供內(nèi)置類型的包裝類型,并且實(shí)現(xiàn) java 那樣的自動拆裝箱。另一條路是支持類似 rust 的運(yùn)算符重載,例如add代表+,mul代表*,這樣只需要將內(nèi)置運(yùn)算符進(jìn)行簡單的映射即可兼容內(nèi)置類型,同時又能滿足自定義類型。不過鑒于 golang 官方一直對運(yùn)算符重載持否定態(tài)度,方案 2 也只能想想了。

          另一個黑暗面就是泛型如何實(shí)現(xiàn),現(xiàn)有的主流方案不是類型擦除(java,typescript),就是將泛型代碼看作模板進(jìn)行實(shí)例化代碼生成(c++,rust),另外還有個另類的 c#在運(yùn)行時進(jìn)行實(shí)例化。

          目前社區(qū)仍然偏向于模板替換,采用類型字典的方案暫時無法處理泛型 struct,實(shí)現(xiàn)也非常復(fù)雜,所以反對聲不少。如果最終敲定了模板方案,那么 golang 要面對的新問題就是鏈接時間過長和代碼膨脹了。一份泛型代碼可以生產(chǎn)數(shù)份相同的實(shí)例,這些實(shí)例需要在鏈接階段被鏈接器剔除,這會導(dǎo)致鏈接時間爆增。代碼膨脹是老生常談的問題了,更大的二進(jìn)制文件會導(dǎo)致啟動更慢,代碼里的雜音更多導(dǎo)致 cpu 緩存利用率的下降。

          鏈接時間的優(yōu)化社區(qū)有人提議可以在編譯期標(biāo)記各個實(shí)例提前去重,因?yàn)?golang 各個代碼直接是有清晰的聯(lián)系的,不像 c++文件之間單獨(dú)編譯最終需要在鏈接階段統(tǒng)一處理。代碼膨脹目前沒有辦法,而且代碼膨脹會不會對性能產(chǎn)生影響,影響多大能否限定在可接受范圍都還是未知數(shù)。

          但不管怎么說,我們都需要泛型,因?yàn)閹淼倪h(yuǎn)比失去的要多。

          作者:@apocelipes

          本文為作者原創(chuàng),轉(zhuǎn)載請注明出處:https://www.cnblogs.com/apocelipes/p/13832224.html

          參考

          https://colobu.com/2016/04/14/Golang-Generics-Proposal/

          https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md

          https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md

          https://blog.golang.org/why-generics

          https://blog.golang.org/generics-next-step

          https://github.com/golang/proposal/blob/master/design/generics-implementation-gcshape.md

          https://stackoverflow.com/questions/4184954/are-there-standard-queue-implementations-for-c



          推薦閱讀


          福利

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

          瀏覽 68
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  精品婷婷色一区二区三区蜜桃 | SM在线免费观看 | 天天av天天看 | 久久成人电影院 | 91干伦理片 |