C# 泛型約束 new() 你必須要知道的事


C# 泛型約束 new() 你必須要知道的事

注意:本文不會講泛型如何使用,關於泛型的概念和泛型約束的使用請移步谷歌。

本文要講的是關於泛型約束無參構造函數 new 的一些底層細節和注意事項。寫這篇文章的原因也是因為看到 github 上,以及其他地方看到的代碼都是那么寫的,而我一查相關的資料,發現鮮有人提到這方面的細節,所以才有了此文。

這里我先直接拋出一段代碼,請大家看下這段代碼有什么問題?或者說能說出什么問題?

public static T CreateInstance<T>() where T: new() => new T();

先不要想這種寫法的合理性(實際上很多人都會諸如此類的這么寫,無非就是中間多了一些業務處理,最后還是會 return new T())。先想一下,然后在看下面的分析。

假設這樣的問題出現在面試上,其實能有很多要考的點。

首先是泛型約束的底層細節

如果說我們不知道泛型底下到底做了什么操作,我們也不用急,我們可以用 ILSpy 來看查看一下,代碼片段如下:

.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<!!T>()
    IL_0005: ret
} // end of method C::CreateInstance

沒有 ILSpy 的同學可以移步這里在線查看

在 IL_0000 就能明顯看出泛型約束 new() 的底層實現是通過反射來實現的。至於 System.Activator.CreateInstance<T> 方法實現我在這里就不提了。只知道這里用的是它就足夠了。不知道大家看到這里有沒有覺得一絲驚訝,我當時是有被驚到的,因為我的第一想法就是覺得這么簡單肯定是直接調用無參 .ctor,居然是用到的反射。畢竟編譯器擁有在編譯器就能識別具體的泛型類了。現在可以馬后炮的講:正因為是編譯器只有在編譯期才確定具體泛型類型,所以編譯器無法事先知道要直接調用哪些無參構造函數類,所以才用到了反射。

關於 System.Activator.CreateInstance<T>() 的方法描述,在微軟官網api中的remark部分有提到

如果本文僅僅只是這樣,那我肯定沒有勇氣寫下這片文章的。因為其實已經有人早在 04 年園子里就提到了這一點。但是我查到的資料也就止步於此。

試想一下 ,如果你的框架中有些方法用到了無參構造函數泛型約束,並且處於調用的熱路徑上,其實這樣性能是大打折扣的,因為反射 Activator.CreateInstance 性能肯定是遠遠不如直接調用無參構造函數的。

注意,我這里說的反射是通俗的概念,因為我找不到CLR內部方法實現的代碼,其實現過程細節有同學陳鑫偉在評論中指出來了。

那么有沒有什么方法能夠在使用泛型約束這個特征的同時,又不會讓編譯器去用反射呢?

答案肯定是有的,這點我想喜歡動手實驗肯定早就知道了。其實我們可以用到委托來初始化類

泛型約束 return new T() 的優化——委托

如果大家對這點都知道的話,可以略過本節(在這里鼓勵大家可以寫出來造福大家呀,對於這點那些不知道的人(我)要花很長時間才弄清楚 -_-)。

讓我們把上面的例子改成如下方式:

public static Func<Bar> InstanceFactory => () => new Bar();

對於委托的底層相信大家還是都知道的,底層是通過生成一個類 C,在這個類中直接實例化類 Bar。下面我只貼出關鍵的代碼片段

.method public hidebysig specialname static 
    class [System.Private.CoreLib]System.Func`1<class Bar> 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<class Bar> 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'::'<get_InstanceFactory>b__3_0'()
    IL_0014: newobj instance void class [System.Private.CoreLib]System.Func`1<class Bar>::.ctor(object, native int)
    IL_0019: dup
    IL_001a: stsfld class [System.Private.CoreLib]System.Func`1<class Bar> C/'<>c'::'<>9__3_0'

    IL_001f: ret
} // end of method C::get_InstanceFactory

.method assembly hidebysig 
    instance class Bar '<get_InstanceFactory>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'::'<get_InstanceFactory>b__3_0'

同樣我們可以通過 ILSpy 或者 在線查看示例 查看委托生成的代碼。

這里可以明顯看出是不存在反射調用的,IL_000e 處直接調用編譯器生成的類 C 的方法 b__3_0 ,在這個方法中就會直接調用類 Bar 的構造函數。所以性能上絕對要比上種寫法要高得多。

看到這里可能大家又有新問題了,眾所周知,委托要在初始化時就要確定表達式。所以與此處的泛型動態調用是沖突的。的確沒錯,委托必須要在初始化表達式時就要確定類型。但是我們現在已經知道了委托是能夠避免讓編譯器不用反射的,剩下的只是解決動態表達式的問題,毫無疑問表達式樹該登場了。

泛型約束 return new T() 的優化——表達式樹

對於這部分已經知道的同學可以跳過本節。

把委托改造成表達式樹那是非常簡單的,我們可以不假思索的寫出下面代碼:

private static readonly Expression<Func<T>> 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
注意:我上傳這一版是下方第一個文章給出的例子的整理之后的版本。文中有很多代碼我都沒貼出來,一是覺得意義不大,重要的是思考過程和實踐過程,還占文章篇幅。二是還是想讓不知道這些的同學能自己動手編碼自己的版本,最后才看與那些大牛寫的版本的差距在哪,這樣才會更有收獲。

性能測試對比結果


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

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM