0 簡述
Go語言並不支援範型程式設計(某些內建函式是支援範型的,但是使用者自定義函式不支援範型),但是可以藉助reflect來一定程度上彌補這部分能力的缺失,因為要靠執行時計算所以有執行時開銷,效能比不上真正的範型實現。
Java支援真正的“範型”,泛型程式設計的好處是,編譯時對型別安全性進行檢查,並且模板引數可以是任意型別不用做型別轉換,既安全又方便。由於是在編譯時進行型別檢查,並且Java編譯器會對類、方法、變數級別的模板引數進行型別擦除(Type Erasure,簡單理解就是將模板引數替換成Object型別或者第一個Bound的型別),無執行時開銷,比Go藉助反射模擬範型效能好,也不用像C++一樣拷貝程式碼引起編譯速度下降或者程式碼尺寸膨脹。點選檢視:Java-Type-Erasure。
C++通過“模板”來支援“範型”程式設計,之所以加引號,是因為C++不是真正的支援範型程式設計,模板特例化時編譯器其實是生成了一個新類的程式碼。C++模板是通過Macro來進行處理的,相當於複製、貼上了類别範本的程式碼,並替換了模板引數型別。簡言之,就是一個高階的Macro處理系統。但是因為拷貝了程式碼,程式碼膨脹導致了編譯速度下降、檔案尺寸增加。
網上有很多相關的討論,這裡舉個示例簡單總結一下。
1 C++ 類别範本
#include <iostream>
using namespace std;
template <typename T>
class Calc{
T t1;
T t2;
public:
T Add(T t1, T t2) {
return t1 + t2;
}
};
int main() {
Calc<int> calc_int;
auto sum_int = calc_int.Add(1, 2);
cout << "1 + 2 = " << sum_int << endl;
Calc<float> calc_flt;
auto sum_flt = calc_flt.Add(1.1, 2.2);
cout << "1.1 + 2.2 = " << sum_flt << endl;
return 0;
}
這裡其實是建立了兩個不同的類,objdump -dS
可以很清晰地看到至少建立了兩個不同的方法Add(T, T)
,可能會有人認為這是函式過載中的name mangling
,其實不是,確實是生成了兩個不同的型別,這個可以通過DWARF相關資訊看出來,首先g++ -s main.cpp
得到彙編後檔案main.s,然後檢視該檔案內容並搜尋Calc,下面兩個分別表示Calc<float>模板例項
以及Calc<int>模板例項
,二者確實屬於兩個不同的型別,一個是用Ltypes95來標識,一個是用Ltypes47來標識。
2 Java範型
Java中範型的實現依賴於Java中的類繼承機制、型別擦除、型別轉換來實現,最終只會有一個類的示例。
編譯時,編譯器會對模板引數T進行型別擦除,這裡有兩種處理的情形:
- 模板引數T,沒有繫結一個型別(如
T extends Comparable
),那麼型別擦除後,模板引數T會用Object進行替代,同時生成對應的型別轉換的程式碼; - 模板引數T,有限制型別(如T extends Comparable限定了模板實參必須實現Comparable介面),那麼型別擦除後,模板引數T就用這第一個bound的型別Comparable代替,同時生成對應的型別轉換程式碼。
需要注意的是,編譯時型別擦除雖然會對原始碼做一定的調整,某些資訊看似丟失了,比如List<String> lst
被擦除後變為了List<Object> lst
,在執行時我們依然可以通過反射機制來獲取lst的元素型別為String,則是為什麼呢?這是因為型別擦除並不是刪除所有型別資訊,模板實參的資訊會以某種形式儲存下來,以便反射時使用。
// 型別擦除前程式碼
List<String> lst = new ArrayList();
lst.Add("hello");
lst.Add("world");
Iterator it = lst.iterator();
for ; it.hasNext(); {
String el = it.Next();
}
// 型別擦除後程式碼
List lst = new ArrayList(); // 模板實參String,擦除為Object
lst.Add("hello"); // hello為String,IS-A Object關係成立
lst.Add("world"); // ...
Iterator it = lst.iterator();
for ; it.hasNext(); {
String el = (String) it.Next(); // 編譯器自動插入型別轉換的程式碼
}
由此可見,Java的範型實現,既不會像C++那樣多建立類導致程式碼體積膨脹,也不會帶來執行時開銷,也沒有破壞反射依賴的資訊。
3 go反射
Go1不支援範型,但是它可以結合interface{}以及reflection來模擬範型。反射的效能大約有幾百ns級別的效能損耗,和範型實現比,還是存在一定的效能差距。
Go2已經中已經計劃支援泛型了,拭目以待。
對“泛型”這個寬泛的術語,對比了c++、java、go的一些支援和實現上的差異。
本作品採用《CC 協議》,轉載必須註明作者和本文連結