簡介
泛型參考資料爛大街,基本資料不再贅述,比如泛型接口/委托/方法的使用,逆變與協(xié)變。
泛型好處有如下幾點(diǎn)
- 代碼重用
算法重用,只需要預(yù)先定義好算法,排序,搜索,交換,比較等。任何類型都可以用同一套邏輯 - 類型安全
編譯器保證不會(huì)將int傳給string - 簡單清晰
減少了類型轉(zhuǎn)換代碼 - 性能更強(qiáng)
減少裝箱/拆箱,泛型算法更優(yōu)異。
為什么說泛型性能更強(qiáng)?
主要在于裝箱帶來的托管堆分配問題以及性能損失
- 值類型裝箱會(huì)額外占用內(nèi)存
var a = new List<int>()
{
1,2, 3, 4
};
var b = new ArrayList()
{
1,2,3,4
};
變量a:72kb
![](/files/attmgn/2025/1/freeflydom20250108110652303_0.jpg)
變量b:184kb
![](/files/attmgn/2025/1/freeflydom20250108110652510_1.jpg)
- 裝箱/拆箱會(huì)消耗額外的CPU
public void ArrayTest()
{
Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();
ArrayList arrayList = new ArrayList();
for (int i = 0; i < 10000000; i++)
{
arrayList.Add(i);
_ = (int)arrayList[i];
}
stopwatch.Stop();
Console.WriteLine($"array time is {stopwatch.ElapsedMilliseconds}");
}
public void ListTest()
{
Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();
List<int> list = new List<int>();
for (int i = 0; i < 10000000; i++)
{
list.Add(i);
_ = list[i];
}
stopwatch.Stop();
Console.WriteLine($"list time is {stopwatch.ElapsedMilliseconds}");
}
![](/files/attmgn/2025/1/freeflydom20250108110652553_2.jpg)
如此巨大的差異,無疑會(huì)造成GC的管理成本增加以及額外的CPU消耗。
思考一個(gè)問題,如果是引用類型的實(shí)參。差距還會(huì)如此之大嗎?
如果差距不大,那我們使用泛型的理由又是什么呢?
開放/封閉類型
CLR中有多種類型對(duì)象 ,比如引用類型,值類型,接口類型和委托類型,以及泛型類型。
根據(jù)創(chuàng)建行為,他們又被分為開放類型/封閉類型
為什么要說到這個(gè)? 泛型的一個(gè)有優(yōu)點(diǎn)就是代碼復(fù)用,只要定義好算法。剩下的只要往里填就好了。比如List<>開放給任意實(shí)參,大家都可以復(fù)用同一套算法。
舉個(gè)例子
- 開放類型是指類型參數(shù)尚未被指定,他們不能被實(shí)例化 List<>,Dictionary<,>,interface 。它們只是搭建好了基礎(chǔ)框架,開放不同的實(shí)參
Type it = typeof(ITest);
Activator.CreateInstance(it);
Type di = typeof(Dictionary<,>);
Activator.CreateInstance(di);
- 封閉類型是指類型已經(jīng)被指定,是可以被實(shí)例化 List<string>,String 就是封閉類型。它們只接受特定含義的實(shí)參
Type li = typeof(List<string>);
Activator.CreateInstance(li);
代碼爆炸
所以當(dāng)我們使用開放類型時(shí),都會(huì)面臨一個(gè)問題。在JIT編譯階段,CLR會(huì)獲取泛型的IL,再尋找對(duì)應(yīng)的實(shí)參替換,生成合適的本機(jī)代碼。
但這么做有一個(gè)缺點(diǎn),要為每一種不同的泛型類型/方法組合生成,各種各種的本機(jī)代碼。這將明顯增加程序的Assembly,從而損害性能
CLR為了緩解該現(xiàn)象,做了一個(gè)特殊的優(yōu)化:共享方法體
相同類型實(shí)參,共用一套方法
如果一個(gè)Assembly中使用了List<Struct>另外一個(gè)Assembly也使用了List<Struct>
那么CLR只會(huì)生成一套本機(jī)代碼。
引用類型實(shí)參,共用一套方法
List<String>與List<Stream> 實(shí)參都是引用類型,它們的值都是托管堆上的指針引用。因此CLR對(duì)指針都可以用同一套方式來操作
值類型就不行了,比如int與long. 一個(gè)占用4字節(jié),一個(gè)占用8字節(jié)。占用的內(nèi)存不長不一樣,導(dǎo)致無法用同一套邏輯來復(fù)用
眼見為實(shí)1
示例代碼
internal class Program
{
static void Main(string[] args)
{
var a = new Test<string>();
var b = new Test<Stream>();
Debugger.Break();
}
}
public class Test<T>
{
public void Add(T value)
{
}
public void Remove(T value)
{
}
}
變量a:
![](/files/attmgn/2025/1/freeflydom20250108110652606_3.jpg)
變量b
![](/files/attmgn/2025/1/freeflydom20250108110652645_4.jpg)
仔細(xì)觀察發(fā)現(xiàn),它們的EEClass完全一致,它們的Add/Remove方法的MethodDesc也完全一直。這印證了上面的說法,引用類型實(shí)參引用同一套方法。
眼見為實(shí)2
點(diǎn)擊查看代碼
internal class Program
{
static void Main(string[] args)
{
var a = new Test<int>();
var b = new Test<long>();
var c = new Test<MyStruct>();
Debugger.Break();
}
}
public class Test<T>
{
public void Add(T value)
{
}
public void Remove(T value)
{
}
}
public struct MyStruct
{
public int Age;
}
我們?cè)侔岩妙愋蛽Q為值類型,再看看它們的方法表。
變量a:
![](/files/attmgn/2025/1/freeflydom20250108110652730_5.jpg)
變量b:
![](/files/attmgn/2025/1/freeflydom20250108110652824_6.jpg)
變量c:
![](/files/attmgn/2025/1/freeflydom20250108110652929_7.jpg)
一眼就能看出,它們的MethodDesc完全不一樣。這說明在Assembly中。CLR為泛型生成了3套方法。
細(xì)心的朋友可能會(huì)發(fā)現(xiàn),引用類型的實(shí)參變成了一個(gè)叫System.__Canon的類型。CLR 內(nèi)部使用 System.__Canon 來給所有的引用類型做“占位符”使用
有興趣的小伙伴可以參考它的源碼:coreclr\System.Private.CoreLib\src\System__Canon.cs
為什么值類型無法共用同一套方法?
其實(shí)很好理解,引用類型的指針長度是固定的(32位4byte,64位8byte),而值類型的長度不一樣。導(dǎo)致值類型生成的底層匯編無法統(tǒng)一處理。因此值類型無法復(fù)用同一套方法。
眼見為實(shí)
?點(diǎn)擊查看代碼
internal class Program
{
static void Main(string[] args)
{
var a = new Test<int>();
a.Add(1);
var b = new Test<long>();
b.Add(1);
var c = new Test<string>();
c.Add("");
var d = new Test<Stream>();
d.Add(null);
Debugger.Break();
}
}
public class Test<T>
{
public void Add(T value)
{
var s = value;
}
public void Remove(T value)
{
}
}
//變量a
00007FFBAF7B7435 mov eax,dword ptr [rbp+58h]
00007FFBAF7B7438 mov dword ptr [rbp+2Ch],eax //int 類型步長4 2ch
//變量b
00007FFBAF7B7FD7 mov rax,qword ptr [rbp+58h]
00007FFBAF7B7FDB mov qword ptr [rbp+28h],rax //long 類型步長8 28h 匯編不一致
//變量c
00007FFBAF7B8087 mov rax,qword ptr [rbp+58h]
00007FFBAF7B808B mov qword ptr [rbp+28h],rax // 28h
//變量d
00007FFBAF7B8087 mov rax,qword ptr [rbp+58h]
00007FFBAF7B808B mov qword ptr [rbp+28h],rax // 28h 引用類型地址步長一致,匯編也一致。
泛型的數(shù)學(xué)計(jì)算
在.NET 7之前,如果我們要利用泛型進(jìn)行數(shù)學(xué)運(yùn)算。是無法實(shí)現(xiàn)的。只能通過dynamic來曲線救國
![](/files/attmgn/2025/1/freeflydom20250108110652971_8.jpg)
.NET 7中,引入了新的數(shù)學(xué)相關(guān)泛型接口,并提供了接口的默認(rèn)實(shí)現(xiàn)。
![](/files/attmgn/2025/1/freeflydom20250108110652992_9.jpg)
https://learn.microsoft.com/zh-cn/dotnet/standard/generics/math
數(shù)學(xué)計(jì)算接口的底層實(shí)現(xiàn)
C#層:
相加的操作主要靠IAdditionOperators接口。
![](/files/attmgn/2025/1/freeflydom20250108110653015_10.jpg)
IL層:
+操作符被JIT編譯成了op_Addition抽象方法
![](/files/attmgn/2025/1/freeflydom20250108110653037_11.jpg)
對(duì)于int來說,會(huì)調(diào)用int的實(shí)現(xiàn)
System.Int32.System.Numerics.IAdditionOperators
![](/files/attmgn/2025/1/freeflydom20250108110653059_12.jpg)
對(duì)于long來說,會(huì)調(diào)用long的實(shí)現(xiàn)
System.Int64.System.Numerics.IAdditionOperators
![](/files/attmgn/2025/1/freeflydom20250108110653083_13.jpg)
從原理上來說很簡單,BCL實(shí)現(xiàn)了基本值類型的所有+-*/操作,只要在泛型中做好約束,JIT會(huì)自動(dòng)調(diào)用相應(yīng)的實(shí)現(xiàn)。
結(jié)論
泛型,用就完事了。就是要稍微注意(硬盤比程序員便宜多了)值類型泛型造成的代碼爆炸。