C# 泛型约束 new() 你必须要知道的事
这里我先直接抛出一段代码
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 8
IL_0000: call !!0 [System.Private.CoreLib]System.Activator::CreateInstance<!!T>()
IL_0005: ret
} // end of method C::CreateInstance
在 IL_0000 就能明显看出泛型约束 new() 的底层实现是通过反射来实现的。至于 System.Activator.CreateInstance<T>
方法实现我在这里就不提了。
只知道这里用的是它就足够了。不知道大家看到这里有没有觉得一丝惊讶,我当时是有被惊到的,因为我的第一想法就是觉得这么简单肯定是直接调用无参 .ctor,居然是用到的反射。毕竟编译器拥有在编译器就能识别具体的泛型类了。
现在可以马后炮的讲:正因为是编译器只有在编译期才确定具体泛型类型,所以编译器无法事先知道要直接调用哪些无参构造函数类,所以才用到了反射。
关于 System.Activator.CreateInstance<T>()
的方法描述,在微软官网api中的remark部分有提到。
如果本文仅仅只是这样,那我肯定没有勇气写下这片文章的。因为其实已经有人早在 04 年园子里就提到了这一点。但是我查到的资料也就止步于此。
试想一下 ,如果你的框架中有些方法用到了无参构造函数泛型约束,并且处于调用的热路径上,其实这样性能是大打折扣的,因为反射 Activator.CreateInstance
性能肯定是远远不如直接调用无参构造函数的。
总结
其实如果面试真的有问到这个问题的话,其实考的就是对泛型约束 new() 底层的一个熟悉程度,然后转而从反射的点来思考问题的优化方案。
因为这可以散发出很多问题,比如性能优化,从直接返回 new T()
到委托,因为委托无法做到动态变化,所以想到了表达式树。
那么我们继而也能举一反三的知道,如果要继续优化的话,在构造表达式树时,我们可以用缓存来节省每次调用方法的构造表达式树的时间(DI 的 CallSite 实现细节就是如此)。
如果我们生思熟虑之后还要选择继续优化,那么我们还可以从表达式树转到动态生成代码这一领域,通过编写 IL 代码来生成表达式树,进而缓存下来达到近乎直接调用的性能。这也是为什么我花了很长时间弄清楚这个的原因。