.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<Employee>();

使用 Microsoft.Extensions.DependencyInjection

接下來就是在.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

Natasha 是基於 Roslyn 開發的動態程序集構建庫,直觀和流暢的 Fluent API 設計,通過 roslyn 的強大賦能, 可以在程序運行時創建代碼,包括 程序集、類、結構體、枚舉、接口、方法等, 用來增加新的功能和模塊,這裡我們用 NInstance 來創建對象。

// Natasha 初始化
NatashaInitializer.Initialize();

Employee employee = Natasha.CSharp.NInstance.Creator<Employee>().Invoke();

使用表達式 Expression

表達式 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對象

使用 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/

然後使用 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/

https://github.com/dotnetcore/Natasha

到此這篇關於.NET中創建對象的幾種方式和對比的文章就介紹到這瞭,更多相關.NET 創建對象內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: