在 .net 中,建立一個對象最簡單的方法是直接使用 new (), 在實際的項目中,咱們可能還會用到反射的方法來建立對象,若是你看過 Microsoft.Extensions.DependencyInjection
的源碼,你會發現,爲了保證在不一樣場景中的兼容性和性能,內部使用了多種反射機制。在本文中,我對比了常見的幾種反射的方法,介紹了它們分別應該如何使用,每種的簡易度和靈活度,而後作了基準測試,一塊兒看看這之間的性能差距。git
我按照使用的簡易度和靈活度,作了下邊的排序,可能還有一些其餘的反射方式,好比 Source Generators,本文中只針對如下幾種進行測試。github
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 對象。express
這是使用反射的最簡單和最靈活的方法之一,由於能夠使用相似的方法來調用對象的方法、接口和屬性等,可是這個也是最慢的反射方法之一。數組
若是你須要建立對象的話,在.NET Framework 和 .NET Core 中正好有一個專門爲此設計的靜態類,System.Activator, 使用方法很是的簡單,還能夠使用泛型,並且你還能夠傳入其餘的參數。ide
Employee employee = Activator.CreateInstance<Employee>();
接下來就是在.NET Core 中很熟悉的 IOC 容器,Microsoft.Extensions.DependencyInjection,把類型註冊到容器中後,而後咱們使用 IServiceProvider 來獲取對象,這裏我使用了 Transient 的生命週期,保證每次都會建立一個新的對象函數
IServiceCollection services = new ServiceCollection(); services.AddTransient<Employee>(); IServiceProvider provider = services.BuildServiceProvider(); Employee employee = provider.GetService<Employee>();
Natasha 是基於 Roslyn 開發的動態程序集構建庫,直觀和流暢的 Fluent API 設計,經過 roslyn 的強大賦能, 能夠在程序運行時建立代碼,包括 程序集、類、結構體、枚舉、接口、方法等, 用來增長新的功能和模塊,這裏咱們用 NInstance 來建立對象。性能
// Natasha 初始化 NatashaInitializer.Initialize(); Employee employee = Natasha.CSharp.NInstance.Creator<Employee>().Invoke();
表達式 Expression 其實也已經存在很長時間了,在 System.Linq.Expressions 命名空間下, 而且是各類其餘功能 (LINQ) 和庫(EF Core) 不可或缺的一部分,在許多方面,它相似於反射,由於它們容許在運行時操做代碼。測試
NewExpression constructorExpression = Expression.New(typeof(Employee)); Expression<Func<Employee>> lambdaExpression = Expression.Lambda<Func<Employee>>(constructorExpression); Func<Employee> func = lambdaExpression.Compile(); Employee employee = func();
表達式提供了一種用於聲明式代碼的高級語言,前兩行建立了的表達式, 等價於 () => new Employee(),而後調用 Compile 方法獲得一個 Func<> 的委託,最後調用這個 Func 返回一個Employee對象ui
Emit 主要在 System.Reflection.Emit 命名空間下,這些方法容許咱們在程序中直接建立 IL (中間代碼) 代碼,IL 代碼是指編譯器在編譯程序時輸出的 "僞彙編代碼", 也就是編譯後的dll,當程序運行的時候,.NET CLR 中的 JIT編譯器 將這些 IL 指令轉換爲真正的彙編代碼。spa
接下來,須要在運行時建立一個新的方法,很簡單,沒有參數,只是建立一個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/
而後使用 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<Employee> emitActivator = dynamic.CreateDelegate(typeof(Func<Employee>)) as Func<Employee>; 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<Employee> _expressionActivator; private readonly Func<Employee> _emitActivator; private readonly Func<Employee> _natashaActivator; public ReflectionBenchmarks() { _ctor = typeof(Employee).GetConstructor(Type.EmptyTypes); _provider = new ServiceCollection().AddTransient<Employee>().BuildServiceProvider(); NatashaInitializer.Initialize(); _natashaActivator = Natasha.CSharp.NInstance.Creator<Employee>(); _expressionActivator = Expression.Lambda<Func<Employee>>(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<Employee>)) as Func<Employee>; } [Benchmark(Baseline = true)] public Employee UseNew() => new Employee(); [Benchmark] public Employee UseReflection() => _ctor.Invoke(null) as Employee; [Benchmark] public Employee UseActivator() => Activator.CreateInstance<Employee>(); [Benchmark] public Employee UseDependencyInjection() => _provider.GetRequiredService<Employee>(); [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<ReflectionBenchmarks>(); } } }
這裏的環境是 .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/