C#的泛型约束中,new()约束算是最常用也最容易被误解的一个。它允许开发者写出new T()这样的代码,看起来和普通的new没什么区别,但底层实现却完全不同。很多人在实现泛型工厂时随手写下where T : new()就以为万事大吉,结果在高频调用场景下发现性能问题,才回头去研究它的实现机制。本文将从底层原理出发,把new()约束的工作方式讲透,并给出几种实用的工厂模式实现方案。

一、new()约束的底层实现原理
首先需要明确一点:C#中的泛型分为两种形态,一种是运行时 JIT 为每个具体值类型单独生成一份机器码的特化泛型,另一种是所有引用类型共享同一份代码的共享泛型。对于引用类型,CLR 采用共享代码策略,即所有引用类型参数共用同一个泛型方法体。这就带来一个问题:JIT 在编译泛型方法时,根本不知道 T 具体是什么类型,那 new T() 是怎么执行的?
答案是:编译器会把 new T() 翻译成对 Activator.CreateInstance<T>() 的调用。这个方法内部会查找 T 的默认构造函数元数据,通过反射完成实例化。也就是说,new T() 在 MSIL 层面并不是一个真正的 newobj 指令,而是一次带缓存的反射调用。来看一段验证代码:
public class Factory<T> where T : new()
{
public T Create()
{
// 编译后等价于调用 Activator.CreateInstance<T>()
return new T();
}
}
用 ildasm 或者 sharplab 查看这段代码的 IL,你会看到 Create 方法体内是 call Activator.CreateInstance<T>() 而不是 newobj。这就解释了为什么带 new() 约束的对象创建通常比直接 new 慢——它走了反射路径。不过需要说明的是,CLR 对 Activator.CreateInstance 做了相当多的缓存优化,第一次调用之后,构造函数的句柄会被缓存起来,后续调用的开销大幅降低,通常只比直接 new 多出一次委托调用的量级。
还有一个容易忽略的细节:new() 约束要求类型必须有公共无参构造函数。对于值类型,这个约束天然满足,因为所有 struct 都有默认构造语义。而对于引用类型,如果类型只有带参构造函数,编译器会直接报错。这个限制的根源也和底层实现有关——反射查找默认构造函数时必须能找到匹配的元数据。
二、性能对比与瓶颈分析
既然 new T() 本质是反射,我们来量化一下性能差距。基准测试通常显示:直接 new 一个对象大约几纳秒,new T() 在预热后大约是它的三到五倍,而未经缓存的 Activator.CreateInstance(typeof(T)) 首次调用可能达到微秒级别。对于绝大多数业务代码,这个差距完全可以忽略;但在每秒创建百万级对象的场景下(比如对象池、序列化器、ORM 实体实例化),差距就会被放大。
public class Person { public string Name { get; set; } }
// 方式一:直接 new,最快
var a = new Person();
// 方式二:泛型 new T(),走缓存后的反射
var b = CreateViaNew<Person>();
// 方式三:直接反射,首次调用最慢
var c = (Person)Activator.CreateInstance(typeof(Person));
static T CreateViaNew<T>() where T : new() => new T();
瓶颈的根源在于:每次调用 Activator.CreateInstance<T> 都要经过一层间接查找,无法被 JIT 内联成直接的构造指令。理解了这一点,优化方向就清晰了——我们要想办法把运行时的动态查找,变成编译期或首次初始化时的一次性工作,之后走纯粹的委托调用。
三、高性能泛型工厂的三种实现方案
方案一:抽象工厂加编译期绑定
最经典的做法是把对象创建从泛型方法中抽出来,交给一个显式的工厂对象。这样 new 发生在具体类型的代码里,JIT 完全知道目标类型,可以生成最优代码。依赖注入容器的内部实现基本就是这个思路:
public interface IFactory<T>
{
T Create();
}
public sealed class PersonFactory : IFactory<Person>
{
public Person Create() => new Person(); // 真正的 new,可被内联
}
public sealed class FactoryHolder<T>
{
public static IFactory<T> Instance;
}
配合静态泛型类,可以为每个 T 缓存一份工厂实例,利用 CLR 对静态泛型类的按类型实例化特性,实现零锁、无查找的创建路径。缺点是需要为每个类型手写工厂类,类型多时代码量可观,通常配合代码生成或 Source Generator 使用。
方案二:表达式树编译成委托
这是最实用的通用方案。思路是用表达式树构造一个 new 表达式,然后编译成委托。编译产物是一个动态方法,JIT 对它的优化程度接近手写代码:
using System.Linq.Expressions;
using System.Collections.Concurrent;
public static class FastFactory
{
private static readonly ConcurrentDictionary<Type, Func<object>> _cache = new();
public static object Create(Type type)
{
var creator = _cache.GetOrAdd(type, t =>
{
var ctor = t.GetConstructor(Type.EmptyTypes);
var body = Expression.New(ctor); // 构造 new 表达式
return Expression.Lambda<Func<object>>(
Expression.Convert(body, typeof(object))).Compile();
});
return creator();
}
}
首次调用时有表达式树构建和编译的开销,之后每次调用就是一个强类型委托,性能与直接 new 几乎持平。C# 的 System.Text.Json、Dapper 等高性能库内部都大量使用这种技术。需要注意的是,Compile() 会占用内存且不可释放,类型数量极大时需要评估内存压力。
方案三:泛型静态类缓存
结合泛型静态类的按类型实例化特性,可以写出既优雅又快的方案:
public static class FactoryCache<T> where T : new()
{
public static readonly Func<T> Create =
() => new T();
}
// 使用方式:首次访问时初始化一次,后续全是委托直调
var person = FactoryCache<Person>.Create();
这个写法看起来还是用了 new T(),但关键区别在于它只会在静态初始化时执行一次,之后所有调用都通过缓存的 Func<T> 委托完成,避开了每次反射查找的路径。如果再配合 Expression.New 生成委托,性能可以做到极致。
四、选型建议与注意事项
三种方案各有适用场景。普通业务代码直接用 where T : new() 即可,CLR 的缓存机制足够应付大多数需求;需要根据 System.Type 动态创建、且类型集合在运行时才确定时,用表达式树编译方案;追求极限性能、类型集合在编译期已知时,用抽象工厂加静态泛型缓存。
最后提醒两个坑:一是 new() 约束无法约束构造函数的访问级别,内部类或私有构造函数的单例模式不能靠它实现,需要改用 Activator.CreateInstance(type, true) 或表达式树;二是值类型的 new T() 返回的是默认值,不会调用你显式定义的无参构造函数,这一点在写通用代码时尤其要小心。理解了这些底层机制,你在设计泛型工厂时就能做到心中有数,既写得优雅,也跑得飞快。