注意:本文不会讲泛型如何使用,关于泛型的概念和泛型约束的使用请微软官方参考链接。

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/new-constraint

本文要讲的是关于泛型约束无参构造函数 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 8IL_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 8IL_0000: ldsfld class [System.Private.CoreLib]System.Func`1<class Bar> C/'<>c'::'<>9__3_0'IL_0005: dupIL_0006: brtrue.s IL_001fIL_0008: popIL_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: dupIL_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 8IL_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

参考资料

  • https://devblogs.microsoft.com/premier-developer/dissecting-the-new-constraint-in-c-a-perfect-example-of-a-leaky-abstraction/

  • https://alexandrnikitin.github.io/blog/dotnet-generics-under-the-hood/

  • https://www.microsoft.com/en-us/research/wp-content/uploads/2001/01/designandimplementationofgenerics.pdf

  • 《编写高性能.NET代码》

作者:沉睡的木木夕

出处:https://www.cnblogs.com/ms27946/

本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。

C# 泛型约束 new() 的底层细节和性能相关推荐

  1. .NET支持的泛型约束

    .NET支持的类型参数约束有以下五种: where T : struct | T必须是一个结构类型 where T : class T必须是一个类(class)类型 where T : new() | ...

  2. 淡谈泛型约束T(转)

    .net泛型约束 2007-10-06 14:08:54|  分类: .net |字号 订阅 所谓泛型,即通过参数化类型来实现在同一份代码上操作多种数据类型.泛型编程是一种编程范式,它利用" ...

  3. SpeedyCloud研发总监李孟:不要让底层细节被上层打败

    随着互联网的发展,对于现代企业来说,DNS与CDN服务的作用正变得愈发重要,网络访问速度决定了前端客户体验,同时也影响着内部业务系统的运行.SpeedyCloud作为一家新晋IaaS云服务供应商,在D ...

  4. C#高级语法之泛型、泛型约束,类型安全、逆变和协变(思想原理)

    一.为什么使用泛型? 泛型其实就是一个不确定的类型,可以用在类和方法上,泛型在声明期间没有明确的定义类型,编译完成之后会生成一个占位符,只有在调用者调用时,传入指定的类型,才会用确切的类型将占位符替换 ...

  5. 第五节:泛型(泛型类、接口、方法、委托、泛型约束、泛型缓存、逆变和协变)

    一. 泛型诞生的背景 在介绍背景之前,先来看一个案例,要求:分别输出实体model1.model2.model3的id和name值,这三个实体有相同的属性名字id和name. 1 public cla ...

  6. TypeScript泛型约束

    泛型约束 有了泛型之后,一个函数或容器类能处理的类型一下子扩到了无限大,似乎有点失控的感觉.所以这里又产生了一个约束的概念.我们可以声明对类型参数进行约束. 我们还拿上文中的student栗子来说,想 ...

  7. 【整理】C#2.0泛型编程之概述、default()方法、别名指定与泛型约束

    泛型概述 首先我们看一个在泛型出现之前通用的数据结构示例 通用数据结构类 Code public class Stack {     object[] m_Items;     public Stac ...

  8. 学习TypeScript 之 Pick与泛型约束

    何为Pick? type Pick<T, K extends keyof T> = {[key in K]: T[key] } 就是从一个复合类型中,取出几个想要的类型的组合,例如: // ...

  9. Ts extends 泛型约束

    extends 泛型约束 我们一般使用extends来继承接口或者类,但是extends还可以用来泛型约束 function getCnames<T extends { name: string ...

最新文章

  1. 关于软件工程的那些事儿————《人·绩效·职业道德》和《一个程序员的生命周期》读后感...
  2. cocos2dx 学习笔记5 关于屏幕大小的问题
  3. XmlSerializer保存xml文件android
  4. K60学习笔记一:PORT端口
  5. Chrome、FireFox浏览器新标签页打开搜索和书签
  6. Gradle从入门到实战 - Groovy基础(by任玉刚)读后笔记
  7. vue+elementui+阿里icon unicode 踩坑
  8. Sublime Text自定制代码片段之 快速生成html结构
  9. 写作进度周报【人人都是产品经理:9039】
  10. PAT_乙级_1012_筱筱
  11. 怎样才能在网上快速赚到钱?
  12. 天津巨富八大家——天成号韩家
  13. InteliJ IDEA2018.2.1X64 破解 使用
  14. SAAS云平台搭建札记: (一) 浅论SAAS多租户自助云服务平台的产品、服务和订单
  15. CH32F203C8T6 32位增强型低功耗Cortex-M3单片机
  16. 浅谈——服务器虚拟化超融合存储
  17. 让信息跨过高山大海,送达人山人海,奈何光纤损耗太大
  18. 整顿一年再次增资近50%,为什么蚂蚁集团要重启IPO?
  19. 设计原则与思想:设计原则12讲
  20. 现在的网页该不该兼容IE6/7

热门文章

  1. idea2021配置svn报错Cannot run program “svn“ (in directory “xxx“):CreateProcess error=2,系统找不到指定的文件
  2. 2019java面试被蹂躏记录,持续更新
  3. zabbix安装报错解决
  4. 5问法:找出问题的根本原因
  5. OO ALV 自定义F4检索帮助
  6. android 模拟器 启动,最好用的安卓模拟器推荐,AS单独启动模拟器
  7. 家庭光伏发电迟迟不见补贴 只好诉诸法庭
  8. finally在什么时候执行
  9. 【附源码】计算机毕业设计JAVA政府机关门禁管理系统
  10. 工控软件及计算机监控系统设计