在 .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 中创建对象的几种方式的对比的更多相关文章
- Java中创建对象的几种方式
Java中创建对象的五种方式: 作为java开发者,我们每天创建很多对象,但是我们通常使用依赖注入的方式管理系统,比如:Spring去创建对象,然而这里有很多创建对象的方法:使用New关键字.使用Cl ...
- Java中创建对象的五种方式
我们总是讨论没有对象就去new一个对象,创建对象的方式在我这里变成了根深蒂固的new方式创建,但是其实创建对象的方式还是有很多种的,不单单有new方式创建对象,还有使用反射机制创建对象,使用clone ...
- 【转】Java中创建对象的5种方式
Java中创建对象的5种方式 作为Java开发者,我们每天创建很多对象,但我们通常使用依赖管理系统,比如Spring去创建对象.然而这里有很多创建对象的方法,我们会在这篇文章中学到. Java中有 ...
- JavaScript中创建对象的三种方式!
JavaScript中创建对象的三种方式! 第一种 利用对象字面量! // 创建对象的三种方式! // 1 对象字面量. var obj = { // 对象的属性和方法! name: 'lvhang' ...
- Java中创建对象的5种方式
作为Java开发者,我们每天创建很多对象,但我们通常使用依赖管理系统,比如Spring去创建对象.然而这里有很多创建对象的方法,我们会在这篇文章中学到. Java中有5种创建对象的方式,下面给出它们的 ...
- js中创建对象的几种方式
创建对象指创建一个object并给这个对象添加属性和方法,有以下几个方式: 最基本的: var Person={}; Person.name='tom'; Person.age='20'; Perso ...
- Java中创建对象的5种方式 &&new关键字和newInstance()方法的区别
转载:http://www.kuqin.com/shuoit/20160719/352659.html 用最简单的描述来区分new关键字和newInstance()方法的区别:newInstance: ...
- Java技术——Java中创建对象的5种方式
此文为译文 原文连接:https://dzone.com/articles/5-different-ways-to-create-objects-in-java-with-ex 0. 前言 作为Jav ...
- Java 中创建对象的 5 种方式!
Java中有5种创建对象的方式,下面给出它们的例子还有它们的字节码 Employee类: class Employee implements Cloneable, Serializable { pri ...
随机推荐
- CUDA统一内存分析
CUDA统一内存分析 PascalMIG 如 NVIDIA Titan X 和 NVIDIA Tesla P100 是第一个包含页 GPUs 定额引擎的 GPUs ,它是统一内存页错误处理和 MIG ...
- Docker App应用
Docker App应用 这是一个实验特性. 实验性功能提供了对未来产品功能的早期访问.这些特性仅用于测试和反馈,因为它们可能在没有警告的情况下在不同版本之间更改,或者可以从将来的版本中完全删除.在生 ...
- 算法训练 区间k大数查询(题解)
资源限制 时间限制:1.0s 内存限制:256.0MB 问题描述 给定一个序列,每次询问序列中第l个数到第r个数中第K大的数是哪个. 输入格式 第一行包含一个数n,表示序列长度. 第二行包 ...
- spring整合mybatis,ioc容器及声明式事务配置
步骤: 1.创建jdbc.properties文件,用来管理存放连接数据库的相关信息 jdbc.properties:jdbc.user=root jdbc.password=123456 jdbc. ...
- 使用IDEA创建Maven项目
一.创建一个普通的Maven项目 1.启动IDEA 2.创建一个Maven项目 3.Maven的目录结构 二.使用模板创建一个MavenWeb项目 1.启动IDEA 2.创建一个MavenWeb项目 ...
- 「题解」300iq Contest 2 B Bitwise Xor
本文将同步发布于: 洛谷博客: csdn: 博客园: 简书. 题目 题目链接:gym102331B. 题意概述 给你一个长度为 \(n\) 的序列 \(a_i\),求一个最长的子序列满足所有子序列中的 ...
- python赋值,深拷贝和浅拷贝的区别
1.赋值 list1=[[1,2],'fei',66] list2=list1 list1 [[1, 2], 'fei', 66] list2 [[1, 2], 'fei', 66] list2.ap ...
- 【逆向实战】ES文件浏览器未授权访问漏洞(CVE-2019-6447)具体分析及利用
/作者:Kali_MG1937 CSDN博客号:ALDYS4 QQ:3496925334 未经许可,禁止转载/ 漏洞简介 CVE-2019-6447是Android端上的一个知名软件:ES文件浏览器的 ...
- apache jmeter下载与安装
JMeter是Apache软件基金会的产品,用于对静态的和动态的资源性能进行测试.jmeter可以运行在多个平台上,如Windows和Linux,本文讲的是在Windows安装jmeter. 工具/原 ...
- 『动善时』JMeter基础 — 48、使用JMeter自身代理录制测试脚本
目录 1.测试计划内包含的元件 2.HTTP代理服务器的设置内容 3.设置浏览器的代理服务器 4.录制脚本 5.查看录制的脚本 6.HTTP代理服务器的排除模式 7.保存脚本 录制脚本只不过是我们日常 ...