簡介
泛型參考資料爛大街,基本資料不再贅述,比如泛型接口/委托/方法的使用,逆變與協變。
泛型好處有如下幾點
- 代碼重用
算法重用,只需要預先定義好算法,排序,搜索,交換,比較等。任何類型都可以用同一套邏輯 - 類型安全
編譯器保證不會將int傳給string - 簡單清晰
減少了類型轉換代碼 - 性能更強
減少裝箱/拆箱,泛型算法更優異。
為什么說泛型性能更強?
主要在于裝箱帶來的托管堆分配問題以及性能損失
- 值類型裝箱會額外占用內存
var a = new List<int>()
{
1,2, 3, 4
};
var b = new ArrayList()
{
1,2,3,4
};
變量a:72kb
變量b:184kb
- 裝箱/拆箱會消耗額外的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}");
}
如此巨大的差異,無疑會造成GC的管理成本增加以及額外的CPU消耗。
思考一個問題,如果是引用類型的實參。差距還會如此之大嗎?
如果差距不大,那我們使用泛型的理由又是什么呢?
開放/封閉類型
CLR中有多種類型對象 ,比如引用類型,值類型,接口類型和委托類型,以及泛型類型。
根據創建行為,他們又被分為開放類型/封閉類型
為什么要說到這個? 泛型的一個有優點就是代碼復用,只要定義好算法。剩下的只要往里填就好了。比如List<>開放給任意實參,大家都可以復用同一套算法。
舉個例子
- 開放類型是指類型參數尚未被指定,他們不能被實例化 List<>,Dictionary<,>,interface 。它們只是搭建好了基礎框架,開放不同的實參
Type it = typeof(ITest);
Activator.CreateInstance(it);
Type di = typeof(Dictionary<,>);
Activator.CreateInstance(di);
- 封閉類型是指類型已經被指定,是可以被實例化 List<string>,String 就是封閉類型。它們只接受特定含義的實參
Type li = typeof(List<string>);
Activator.CreateInstance(li);
代碼爆炸
所以當我們使用開放類型時,都會面臨一個問題。在JIT編譯階段,CLR會獲取泛型的IL,再尋找對應的實參替換,生成合適的本機代碼。
但這么做有一個缺點,要為每一種不同的泛型類型/方法組合生成,各種各種的本機代碼。這將明顯增加程序的Assembly,從而損害性能
CLR為了緩解該現象,做了一個特殊的優化:共享方法體
相同類型實參,共用一套方法
如果一個Assembly中使用了List<Struct>另外一個Assembly也使用了List<Struct>
那么CLR只會生成一套本機代碼。
引用類型實參,共用一套方法
List<String>與List<Stream> 實參都是引用類型,它們的值都是托管堆上的指針引用。因此CLR對指針都可以用同一套方式來操作
值類型就不行了,比如int與long. 一個占用4字節,一個占用8字節。占用的內存不長不一樣,導致無法用同一套邏輯來復用
眼見為實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:
變量b
仔細觀察發現,它們的EEClass完全一致,它們的Add/Remove方法的MethodDesc也完全一直。這印證了上面的說法,引用類型實參引用同一套方法。
眼見為實2
點擊查看代碼
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;
}
我們再把引用類型換為值類型,再看看它們的方法表。
變量a:
變量b:
變量c:
一眼就能看出,它們的MethodDesc完全不一樣。這說明在Assembly中。CLR為泛型生成了3套方法。
細心的朋友可能會發現,引用類型的實參變成了一個叫System.__Canon的類型。CLR 內部使用 System.__Canon 來給所有的引用類型做“占位符”使用
有興趣的小伙伴可以參考它的源碼:coreclr\System.Private.CoreLib\src\System__Canon.cs
為什么值類型無法共用同一套方法?
其實很好理解,引用類型的指針長度是固定的(32位4byte,64位8byte),而值類型的長度不一樣。導致值類型生成的底層匯編無法統一處理。因此值類型無法復用同一套方法。
眼見為實
?點擊查看代碼
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 引用類型地址步長一致,匯編也一致。
泛型的數學計算
在.NET 7之前,如果我們要利用泛型進行數學運算。是無法實現的。只能通過dynamic來曲線救國
.NET 7中,引入了新的數學相關泛型接口,并提供了接口的默認實現。
https://learn.microsoft.com/zh-cn/dotnet/standard/generics/math
數學計算接口的底層實現
C#層:
相加的操作主要靠IAdditionOperators接口。
IL層:
+操作符被JIT編譯成了op_Addition抽象方法
對于int來說,會調用int的實現
System.Int32.System.Numerics.IAdditionOperators
對于long來說,會調用long的實現
System.Int64.System.Numerics.IAdditionOperators
從原理上來說很簡單,BCL實現了基本值類型的所有+-*/操作,只要在泛型中做好約束,JIT會自動調用相應的實現。
結論
泛型,用就完事了。就是要稍微注意(硬盤比程序員便宜多了)值類型泛型造成的代碼爆炸。