在 .NET 中建立物件的幾種方式的對比

在 。net 中,建立一個物件最簡單的方法是直接使用 new (), 在實際的專案中,我們可能還會用到反射的方法來建立物件,如果你看過

Microsoft。Extensions。DependencyInjection

的原始碼,你會發現,為了保證在不同場景中的相容性和效能,內部使用了多種反射機制。在本文中,我對比了常見的幾種反射的方法,介紹了它們分別應該如何使用,每種的簡易度和靈活度,然後做了基準測試,一起看看這之間的效能差距。

我按照使用的簡易度和靈活度,做了下邊的排序,可能還有一些其他的反射方式,比如 Source Generators,本文中只針對以下幾種進行測試。

•直接呼叫 ConstructorInfo 物件的Invoke()方法•使用 Activator。CreateInstance()•使用 Microsoft。Extensions。DependencyInjection•黑科技 Natasha•使用表示式 Expression•使用 Reflection。Emit 建立動態方法

使用標準反射的 Invoke 方法

Type typeToCreate = typeof(Employee);ConstructorInfo ctor = typeToCreate。GetConstructor(System。Type。EmptyTypes);Employee employee = ctor。Invoke(null) as Employee;

第一步是透過 typeof() 獲取物件的型別,你也可以透過 GetType 的方式,然後呼叫 GetConstructor 方法,傳入 System。Type。EmptyTypes 引數,實際上它是一個空陣列 (new Type[0]), 返回 ConstructorInfo物件, 然後呼叫 Invoke 方法,會返回一個 Employee 物件。

這是使用反射的最簡單和最靈活的方法之一,因為可以使用類似的方法來呼叫物件的方法、介面和屬性等,但是這個也是最慢的反射方法之一。

使用 Activator。CreateInstance

如果你需要建立物件的話,在。NET Framework 和 。NET Core 中正好有一個專門為此設計的靜態類,System。Activator, 使用方法非常的簡單,還可以使用泛型,而且你還可以傳入其他的引數。

Employee employee = Activator。CreateInstance();

使用 Microsoft。Extensions。DependencyInjection

接下來就是在。NET Core 中很熟悉的 IOC 容器,Microsoft。Extensions。DependencyInjection,把型別註冊到容器中後,然後我們使用 IServiceProvider 來獲取物件,這裡我使用了 Transient 的生命週期,保證每次都會建立一個新的物件

IServiceCollection services = new ServiceCollection();services。AddTransient();IServiceProvider provider = services。BuildServiceProvider();Employee employee = provider。GetService();

Natasha

Natasha 是基於 Roslyn 開發的動態程式集構建庫,直觀和流暢的 Fluent API 設計,透過 roslyn 的強大賦能, 可以在程式執行時建立程式碼,包括 程式集、類、結構體、列舉、介面、方法等, 用來增加新的功能和模組,這裡我們用 NInstance 來建立物件。

// Natasha 初始化NatashaInitializer。Initialize();Employee employee = Natasha。CSharp。NInstance。Creator()。Invoke();

使用表示式 Expression

表示式 Expression 其實也已經存在很長時間了,在 System。Linq。Expressions 名稱空間下, 並且是各種其他功能 (LINQ) 和庫(EF Core) 不可或缺的一部分,在許多方面,它類似於反射,因為它們允許在執行時操作程式碼。

NewExpression constructorExpression = Expression。New(typeof(Employee));Expression> lambdaExpression = Expression。Lambda>(constructorExpression);Func func = lambdaExpression。Compile();Employee employee = func();

表示式提供了一種用於宣告式程式碼的高階語言,前兩行建立了的表示式, 等價於 () => new Employee(),然後呼叫 Compile 方法得到一個 Func<> 的委託,最後呼叫這個 Func 返回一個Employee物件

使用 Emit

Emit 主要在 System。Reflection。Emit 名稱空間下,這些方法允許我們在程式中直接建立 IL (中間程式碼) 程式碼,IL 程式碼是指編譯器在編譯程式時輸出的 “偽彙編程式碼”, 也就是編譯後的dll,當程式執行的時候,。NET CLR 中的 JIT編譯器 將這些 IL 指令轉換為真正的彙編程式碼。

接下來,需要在執行時建立一個新的方法,很簡單,沒有引數,只是建立一個Employee物件然後直接返回

Employee DynamicMethod(){ return new Employee();}

這裡主要使用到了 System。Reflection。Emit。DynamicMethod 動態建立方法

DynamicMethod dynamic = new(“DynamicMethod”, typeof(Employee), null, typeof(ReflectionBenchmarks)。Module, false);

建立了一個 DynamicMethod 物件,然後指定了方法名,返回值,方法的引數和所在的模組,最後一個引數 false 表示不跳過 JIT 可見性檢查。

我們現在有了方法簽名,但是還沒有方法體,還需要填充方法體,這裡需要C#程式碼轉換成 IL程式碼,實際上它是這樣的

IL_0000: newobj instance void Employee::。ctor()IL_0005: ret

你可以訪問這個站點,它可以很方便的把C#轉換成IL程式碼,

https://sharplab。io/[1]

然後使用 ILGenerator 來操作IL程式碼, 然後建立一個 Func<> 的委託, 最後執行該委託返回一個 Employee 物件

ConstructorInfor ctor = typeToCreate。GetConstructor(System。Type。EmptyTypes);ILGenerator il = createHeadersMethod。GetILGenerator();il。Emit(OpCodes。Newobj, Ctor);il。Emit(OpCodes。Ret);Func emitActivator = dynamic。CreateDelegate(typeof(Func)) as Func;Employee employee = emitActivator();

基準測試

上面我介紹了幾種建立物件的方式,現在我開始使用 BenchmarkDotNet 進行基準測試,我也把 new Employee() 直接建立的方式加到測試列表中,並用它作為 “基線”,來並比較其他的每種方法,同時我把一些方法的預熱操作,放到了建構函式中一次執行,最終的程式碼如下

using BenchmarkDotNet。Attributes;using Microsoft。Extensions。DependencyInjection;using System;using System。Linq。Expressions;using System。Reflection;using System。Reflection。Emit;namespace ReflectionBenchConsoleApp{ public class Employee { } public class ReflectionBenchmarks { private readonly ConstructorInfo _ctor; private readonly IServiceProvider _provider; private readonly Func _expressionActivator; private readonly Func _emitActivator; private readonly Func _natashaActivator; public ReflectionBenchmarks() { _ctor = typeof(Employee)。GetConstructor(Type。EmptyTypes); _provider = new ServiceCollection()。AddTransient()。BuildServiceProvider(); NatashaInitializer。Initialize(); _natashaActivator = Natasha。CSharp。NInstance。Creator(); _expressionActivator = Expression。Lambda>(Expression。New(typeof(Employee)))。Compile(); DynamicMethod dynamic = new(“DynamicMethod”, typeof(Employee), null, typeof(ReflectionBenchmarks)。Module, false); ILGenerator il = dynamic。GetILGenerator(); il。Emit(OpCodes。Newobj, typeof(Employee)。GetConstructor(System。Type。EmptyTypes)); il。Emit(OpCodes。Ret); _emitActivator = dynamic。CreateDelegate(typeof(Func)) as Func; } [Benchmark(Baseline = true)] public Employee UseNew() => new Employee(); [Benchmark] public Employee UseReflection() => _ctor。Invoke(null) as Employee; [Benchmark] public Employee UseActivator() => Activator。CreateInstance(); [Benchmark] public Employee UseDependencyInjection() => _provider。GetRequiredService(); [Benchmark] public Employee UseNatasha() => _natashaActivator(); [Benchmark] public Employee UseExpression() => _expressionActivator(); [Benchmark] public Employee UseEmit() => _emitActivator(); } }

接下來,還修改 Program。cs,注意這裡需要在 Release 模式下執行測試

using BenchmarkDotNet。Running; namespace ReflectionBenchConsoleApp{ public class Program { public static void Main(string[] args) { var sumary = BenchmarkRunner。Run(); } } }

測試結果

在 .NET 中建立物件的幾種方式的對比

這裡的環境是 。NET 6 preview5, 使用標準反射的 Invoke() 方法雖然簡單,但它是最慢的一種,使用 Activator。CreateInstance() 和 Microsoft。Extensions。DependencyInjection() 的時間差不多,時間是直接 new 建立的16倍,使用表示式 Expression 表現最優秀,Natasha 真是黑科技,比用Emit 還快了一點,使用Emit 是直接 new 建立的時間的1。8倍。你應該發現了各種方式之間的差距,但是需要注意的是這裡是 ns 納秒,一納秒是一秒的十億分之一。

這裡簡單對比了幾種建立物件的方法,測試的結果也可能不是特別準確,有興趣的還可以在 。net framework 上面進行測試,希望對您有用!

相關連結

https://andrewlock。net/benchmarking-4-reflection-methods-for-calling-a-constructor-in-dotnet/

https://github。com/dotnetcore/Natasha