最近在看 C++ 的方法和類模板,我就在想 C# 中也是有這個概念的,不過叫法不一樣,人家叫模板,我們叫泛型,哈哈,有點意思,這一篇我們來聊聊它們底層是怎么玩的?
一:C++ 中的模板玩法
畢竟 C++ 是兼容 C 語言,而 C 是過程式的玩法,所以 C++ 就出現了兩種模板類型,分別為:函數模板
和 類模板
,下面簡單分析一下。
1. 函數模板的玩法
玩之前先看看格式:template <typename T> rettype funcname (parameter list) { }
。
說實話,我感覺 C++ 這一點就做的非常好,人家在開頭就特別強調了,這是一個 template
,大家不要搞錯了,按照這個格式,我們來一個簡單的 Sum
操作,參考代碼如下:
#include?<iostream>//求和函數
template?<typename?T>?T?getsum(T??t1,?T??t2)?{return?t1?+?t2;
}int?main()?{int?sum1?=?getsum<int>(10,?10);long?sum2?=?getsum<long>(20,?20);printf("output: int:sum=%d, long: sum=%ld",?sum1,?sum2);
}

接下來我就很好奇,這種玩法和 普通方法
調用有什么不同,要想找到答案,可以用 IDA
去看它的靜態匯編代碼。

從靜態反匯編代碼看,當前生成了兩個函數符號分別為:j_??$getsum@H@@YAHHH@Z
和 j_??$getsum@J@@YAJJJ@Z
,現在我們就搞清楚了,原來一旦給 模板
指定了具體類型,它就生成了一個新的函數符號。
乍一看這句話好像沒什么問題,但如果你心比較細的話,會發現一個問題,如果我調用兩次 getsum<int>
方法,那會生成兩個具體函數嗎?為了尋找答案,我們修改下代碼:
int?main()?{int?sum1?=?getsum<int>(10,?10);int?sum2?=?getsum<int>(15,?15);
}
然后再用 IDA 查看一下。

哈哈,可以發現這時候并沒有生成一個新的函數符號
,其實往細處說:j_??$getsum@H@@YAHHH@Z
?是函數簽名
組合出來的名字,因為它們簽名一致,所以在編譯階段必然就一個了。
2. 類模板的玩法
首先看下類模板的格式:template <typename T1, typename T2, …> class className { };
還是那句話,開頭一個 template
暴擊,告訴你這是一個模板 😄😄😄, 接下來上一段代碼:
#include?<iostream>template?<typename?T>?class?Calculator
{
public:T?getsum(T?a1,?T?b1)?{return?a1?+?b1;}
};int?main()?{Calculator<int>?cal1;int?sum1?=?cal1.getsum(10,?10);Calculator<long>?cal2;int?sum2?=?cal2.getsum(15,?15);printf("output:?sum1=%d,?sum2=%ld",?sum1,sum2);
}
接下來直接看 IDA 生成的匯編代碼。

從上面的方法簽名組織上看,有點意思,類名+方法名
柔和到一個函數符號上去了,可以看到符號不一樣,說明也是根據模板實例化出的兩個方法。
二:C# 中的模板玩法
接下來我們看下 C# 中如何實現 getsum 方法,當我把代碼 copy 到 C# 中,我發現不能實現簡單的 泛型參數
加減乘除操作,這就太搞了,網上找了下實現方式,當然也可以讓 T 約束于 unmanaged
,那就變成指針玩法了。
namespace?ConsoleApp1
{internal?class?Program{static?void?Main(string[]?args){Calculator<int>?calculator1?=?new?Calculator<int>();Calculator<long>?calculator2?=?new?Calculator<long>();int?sum1?=?calculator1.getsum(10,?10);long?sum2?=?calculator2.getsum(15,?15);Console.WriteLine($"sum={sum1},?sum2={sum2}");Console.ReadLine();}}public?class?Calculator<T>?where?T?:?struct,?IComparable{public?T?getsum(T?a1,?T?b1){if?(typeof(T)?==?typeof(int)){int?a?=?(int)Convert.ChangeType(a1,?typeof(int));int?b?=?(int)Convert.ChangeType(b1,?typeof(int));int?c?=?a?+?b;return?(T)Convert.ChangeType(c,?typeof(T));}else?if?(typeof(T)?==?typeof(float)){float?a?=?(float)Convert.ChangeType(a1,?typeof(float));float?b?=?(float)Convert.ChangeType(b1,?typeof(float));float?c?=?a?+?b;return?(T)Convert.ChangeType(c,?typeof(T));}else?if?(typeof(T)?==?typeof(double)){double?a?=?(double)Convert.ChangeType(a1,?typeof(double));double?b?=?(double)Convert.ChangeType(b1,?typeof(double));double?c?=?a?+?b;return?(T)Convert.ChangeType(c,?typeof(T));}else?if?(typeof(T)?==?typeof(decimal)){decimal?a?=?(decimal)Convert.ChangeType(a1,?typeof(decimal));decimal?b?=?(decimal)Convert.ChangeType(b1,?typeof(decimal));decimal?c?=?a?+?b;return?(T)Convert.ChangeType(c,?typeof(T));}return?default(T);}}
}
那怎么去看 Calculator<int>
和 Calculator<long>
到底變成啥了呢?大家應該知道,C# 和 操作系統 隔了一層 C++,所以研究這種遠離操作系統的語言還是有一點難度的,不過既然隔了一層 C++ ,那在 C++ 層面上必然會有所反應。
如果你熟悉 CLR 的類型系統,應該知道 C# 所有的 類 在其上都有一個 MethodTable
類來承載,所以它就是鑒別我們是否生成多個個體的依據,接下來我們用 WinDbg 查看托管堆,看看在其上是如何呈現的。
0:008>?!dumpheap?-stat
Statistics:MT????Count????TotalSize?Class?Name
00007ff9d37638e0????????1???????????24?ConsoleApp1.Calculator`1[[System.Int64,?System.Private.CoreLib]]
00007ff9d3763800????????1???????????24?ConsoleApp1.Calculator`1[[System.Int32,?System.Private.CoreLib]]
從輸出信息看,C++ 層面變成了兩個 methodtable
類,如果不信的化,還可以分別查看 mt 下的所有方法。
0:008>?!dumpmt?-md?00007ff9d37638e0
MethodDesc?TableEntry???????MethodDesc????JIT?Name
...
00007FF9D36924E8?00007ff9d37638d0????JIT?ConsoleApp1.Calculator`1[[System.Int64,?System.Private.CoreLib]]..ctor()
00007FF9D36924E0?00007ff9d37638c0????JIT?ConsoleApp1.Calculator`1[[System.Int64,?System.Private.CoreLib]].getsum(Int64,?Int64)0:008>?!dumpmt?-md?00007ff9d3763800
--------------------------------------
MethodDesc?TableEntry???????MethodDesc????JIT?Name
00007FF9D36924D0?00007ff9d37637f0????JIT?ConsoleApp1.Calculator`1[[System.Int32,?System.Private.CoreLib]]..ctor()
00007FF9D36924C8?00007ff9d37637e0????JIT?ConsoleApp1.Calculator`1[[System.Int32,?System.Private.CoreLib]].getsum(Int32,?Int32)
從輸出信息看,getsum(Int64, Int64)
和 getsum(Int32, Int32)
方法的入口地址 Entry
是完全不一樣的,所以它們是完全獨立的個體。
三:總結
當看到 模板
和 泛型
兩個詞,我感覺前者更 通俗易懂 一些,當給模板
賦予不同類型時將會生成新的實例,在 ?C/C++
中直接化為不同的函數符號,在 C# 中會生成不同的 MethodTable
,由于 C# 遠離機器, 所以盡量談到 C++ 層面即可 🤣🤣🤣