C# 範型約束 new() 你必須要知道的事
阿新 • • 發佈:2020-10-15
# C# 範型約束 new() 你必須要知道的事
注意:本文不會講範型如何使用,關於範型的概念和範型約束的使用請移步谷歌。
本文要講的是關於範型約束無參建構函式 new 的一些底層細節和注意事項。寫這篇文章的原因也是因為看到 github 上,以及其他地方看到的程式碼都是那麼寫的,而我一查相關的資料,發現鮮有人提到這方面的細節,所以才有了此文。
這裡我先直接丟擲一段程式碼,請大家看下這段程式碼有什麼問題?或者說能說出什麼問題?
```c#
public static T CreateInstance() where T: new() => new T();
```
先不要想這種寫法的合理性(實際上很多人都會諸如此類的這麼寫,無非就是中間多了一些業務處理,最後還是會 `return new T()`)。先想一下,然後在看下面的分析。
假設這樣的問題出現在面試上,其實能有很多要考的點。
# 首先是範型約束的底層細節
如果說我們不知道範型底下到底做了什麼操作,我們也不用急,我們可以用 ILSpy 來看檢視一下,程式碼片段如下:
```c#
.method public hidebysig static
!!T CreateInstance<.ctor T> () cil managed
{
// Method begins at RVA 0x2053
// Code size 6 (0x6)
.maxstack 8
IL_0000: call !!0 [System.Private.CoreLib]System.Activator::CreateInstance()
IL_0005: ret
} // end of method C::CreateInstance
```
> 沒有 ILSpy 的同學可以移步[這裡在線檢視](https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgIwFgBQyDMABGgQMIEDeOB1RhyALAQLIAUAlBVTQL5fV+0i6AGwEAKqQBOAUwCGwaQEkAdgGdgs5QGNpAHjEA+dgQDuAC2kzxIAsuknjAXgO3749gG4c3IA=)
在 IL_0000 就能明顯看出範型約束 new() 的底層實現是通過反射來實現的。至於 `System.Activator.CreateInstance` 方法實現我在這裡就不提了。只知道這裡用的是它就足夠了。不知道大家看到這裡有沒有覺得一絲驚訝,我當時是有被驚到的,因為我的第一想法就是覺得這麼簡單肯定是直接呼叫無參 .ctor,居然是用到的反射。畢竟編譯器擁有在編譯器就能識別具體的範型類了。現在可以馬後炮的講:**正因為是編譯器只有在編譯期才確定具體範型型別,所以編譯器無法事先知道要直接呼叫哪些無參建構函式類,所以才用到了反射。**
關於 `System.Activator.CreateInstance()` 的方法描述,在[微軟官網api中的remark部分有提到](https://docs.microsoft.com/en-us/dotnet/api/system.activator.createinstance?redirectedfrom=MSDN&view=netcore-3.1#System_Activator_CreateInstance__1)。
如果本文僅僅只是這樣,那我肯定沒有勇氣寫下這片文章的。因為其實已經[有人早在 04 年園子裡就提到了這一點](https://www.cnblogs.com/Hush/archive/2004/10/07/49674.html)。但是我查到的資料也就止步於此。
試想一下 ,如果你的框架中有些方法用到了無參建構函式範型約束,並且處於呼叫的熱路徑上,其實這樣效能是大打折扣的,因為反射 `Activator.CreateInstance` 效能肯定是遠遠不如直接呼叫無參建構函式的。
> 注意,我這裡說的反射是通俗的概念,因為我找不到CLR內部方法實現的程式碼,其實現過程細節有同學[陳鑫偉](https://www.cnblogs.com/Dogwei/)在評論中指出來了。
那麼有沒有什麼方法能夠在使用範型約束這個特徵的同時,又不會讓編譯器去用反射呢?
答案肯定是有的,這點我想喜歡動手實驗肯定早就知道了。其實我們可以用到**委託來初始化類**。
# 範型約束 return new T() 的優化——委託
如果大家對這點都知道的話,可以略過本節(在這裡鼓勵大家可以寫出來造福大家呀,對於這點那些不知道的人(我)要花很長時間才弄清楚 -_-)。
讓我們把上面的例子改成如下方式:
```c#
public static Func InstanceFactory => () => new Bar();
```
對於委託的底層相信大家還是都知道的,底層是通過生成一個類 C,在這個類中直接例項化類 Bar。下面我只貼出關鍵的程式碼片段
```c#
.method public hidebysig specialname static
class [System.Private.CoreLib]System.Func`1 get_InstanceFactory () cil managed
{
// Method begins at RVA 0x205a
// Code size 32 (0x20)
.maxstack 8
IL_0000: ldsfld class [System.Private.CoreLib]System.Func`1 C/'<>c'::'<>9__3_0'
IL_0005: dup
IL_0006: brtrue.s IL_001f
IL_0008: pop
IL_0009: ldsfld class C/'<>c' C/'<>c'::'<>9'
IL_000e: ldftn instance class Bar C/'<>c'::'b__3_0'()
IL_0014: newobj instance void class [System.Private.CoreLib]System.Func`1::.ctor(object, native int)
IL_0019: dup
IL_001a: stsfld class [System.Private.CoreLib]System.Func`1 C/'<>c'::'<>9__3_0'
IL_001f: ret
} // end of method C::get_InstanceFactory
.method assembly hidebysig
instance class Bar 'b__3_0' () cil managed
{
// Method begins at RVA 0x2090
// Code size 6 (0x6)
.maxstack 8
IL_0000: newobj instance void Bar::.ctor()
IL_0005: ret
} // end of method '<>c'::'b__3_0'
```
> 同樣我們可以通過 ILSpy 或者 [線上檢視示例](https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQMwAJboMLoN7LpGYZQAs6AsgBQCU+hxAvo0ayZnAGzoAqOAJwCmAQwAuQgJIA7AM5iR0gMZCAPLwB8ddAHcAFkOF8Q6aUJ3aAvBtPm+dANzs0nHlACsqgEIiBNmfKKKgBiIkpiAPYCAJ7o1uhWNmY66D4CjsgsSMguWGnIBEhMQA==) 檢視委託生成的程式碼。
這裡可以明顯看出是不存在反射呼叫的,IL_000e 處直接呼叫編譯器生成的類 C 的方法 `b__3_0` ,在這個方法中就會直接呼叫類 Bar 的建構函式。所以效能上絕對要比上種寫法要高得多。
看到這裡可能大家又有新問題了,眾所周知,委託要在初始化時就要確定表示式。所以與此處的範型動態呼叫是衝突的。的確沒錯,委託**必須要在初始化表示式時就要確定型別**。但是我們現在已經知道了委託是能夠避免讓編譯器不用反射的,剩下的只是解決動態表示式的問題,毫無疑問表示式樹該登場了。
# 範型約束 return new T() 的優化——表示式樹
對於這部分已經知道的同學可以跳過本節。
把委託改造成表示式樹那是非常簡單的,我們可以不假思索的寫出下面程式碼:
```c#
private static readonly Expression> ctorExpression = () => new T();
public static T CreateInstance() where T : new() {
var func = ctorExpression.Compile();
return func();
}
```
到這裡其實就有點”舊酒裝新瓶“的意思了。不過有點要注意的是,如果單純只是表示式樹的優化,從執行效率上來看肯定是不如委託來的快,畢竟表示式樹多了一層構造表示式然後編譯成委託的過程。優化也是有的,再繼續往下講就有點“偏題”了。因為往後其實就是對委託,對錶達式樹的效能優化問題。跟範型約束倒沒關係了
# 總結
其實如果面試真的有問到這個問題的話,其實考的就是對範型約束 new() 底層的一個熟悉程度,然後轉而從反射的點來思考問題的優化方案。因為這可以散發出很多問題,比如效能優化,從直接返回 `new T()` 到委託,因為委託無法做到動態變化,所以想到了表示式樹。那麼我們繼而也能舉一反三的知道,如果要繼續優化的話,在構造表示式樹時,我們可以用快取來節省每次呼叫方法的構造表示式樹的時間(DI 的 CallSite 實現細節就是如此)。如果我們生思熟慮之後還要選擇繼續優化,那麼我們還可以從表示式樹轉到動態生成程式碼這一領域,通過編寫 IL 程式碼來生成表示式樹,進而快取下來達到近乎直接呼叫的效能。這也是為什麼我花了很長時間弄清楚這個的原因。
# 最後關於程式碼
程式碼地址在:https://github.com/MarsonShine/Books/tree/master/WHPerformanceDotNet/src/GenericOptimization
注意:我上傳這一版是下方第一個文章給出的例子的整理之後的版本。文中有很多程式碼我都沒貼出來,一是覺得意義不大,重要的是思考過程和實踐過程,還佔文章篇幅。二是還是想讓不知道這些的同學能自己動手編碼自己的版本,最後才看與那些大牛寫的版本的差距在哪,這樣才會更有收穫。
# 效能測試對比結果
``` ini
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.592 (1909/November2018Update/19H2)
Intel Core i5-9400 CPU 2.90GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
.NET Core SDK=5.0.100-rc.1.20452.10
[Host] : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT [AttachedDebugger]
DefaultJob : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT
```
| Method | IterationCount | Mean | Error | StdDev |
|------------------------------- |--------------- |-----------------:|----------------:|----------------:|
| **DirectConstructor** | **1000** | **265.5 ns** | **0.28 ns** | **0.25 ns** |
| GenericConstraintConstructor | 1000 | 34,392.7 ns | 446.07 ns | 417.26 ns |
| DelegateConstructor | 1000 | 6,451.6 ns | 103.58 ns | 91.82 ns |
| ExpressionTreeConstructor | 1000 | 7,500.2 ns | 75.25 ns | 70.39 ns |
| DynamicGenerateCodeConstructor | 1000 | 5,016.4 ns | 49.29 ns | 46.11 ns |
| **DirectConstructor** | **10000000** | **2,576,799.3 ns** | **1,416.08 ns** | **1,105.58 ns** |
| GenericConstraintConstructor | 10000000 | 333,104,316.7 ns | 1,737,941.84 ns | 1,356,870.67 ns |
| DelegateConstructor | 10000000 | 62,633,360.3 ns | 939,353.97 ns | 832,712.83 ns |
| ExpressionTreeConstructor | 10000000 | 74,846,604.8 ns | 689,863.41 ns | 645,298.66 ns |
| DynamicGenerateCodeConstructor | 10000000 | 51,316,999.0 ns | 976,672.25 ns | 1,045,028.36 ns |
# 參考資料
- https://devblogs.microsoft.com/premier-developer/dissecting-the-new-constraint-in-c-a-perfect-example-of-a-leaky-abstraction/
- https://alexandrnikitin.github.io/blog/dotnet-generics-under-the-hood/
- https://www.microsoft.com/en-us/research/wp-content/uploads/2001/01/designandimplementationofgenerics.pdf
- 《編寫高效能.NET程式碼》
#