HOWTO · Csharp

在 C# 中从类型中创建新实例

Activator 类可与 Type 类一起使用,以在 C# 中在运行时创建给定类型的新实例。

本页内容

编写程序时若已知类型,请使用 new 运算符。若在运行时选定或发现了 Type 值,则可使用 Activator.CreateInstance 创建实例,而不必在调用处写死具体类。若构造对象需要特定参数,可使用 Type.GetConstructor 查找匹配的构造函数并调用它。构造函数负责初始化对象;绕过这一步会使对象脱离正常的初始化流程,因此选择哪种方式很重要。

编译时已知 Type 时创建实例

当类属于程序设计的一部分,而且编译器能识别其名称时,请使用 new。例如,Person person = new Person("Ada"); 明确指定了类型和构造函数参数。编译器会检查该类型是否可构造,以及参数是否与某个构造函数匹配。如果类定义了无参数构造函数(或由系统隐式提供了一个),就可以调用 new Person();一旦添加带参数的构造函数,类便不会再获得隐式的无参数构造函数。

对于一般应用程序代码,这通常是最清晰的做法。结构体也可以通过 new 构造,例如其构造函数接受两个整数时,可以写成 new Point(0, 0)。new 会调用选定的构造函数,因此,当你希望执行类的正常初始化逻辑时,应选择这种方式。

使用 Activator.CreateInstance 在运行时创建实例

当程序已取得一个 Type 值,但在构造对象的位置无法写出具体类名时,请使用 Activator.CreateInstance。下面的示例通过一个 Type 值明确展示运行时选择。示例比较了直接构造、无参数激活,以及按参数类型选取构造函数这三种方式。运行以下示例:

本节中的代码以及显示的标签或输出仍为英文。

using System;
using System.Reflection;

public class Person
{
    public string Name { get; }

    public Person()
    {
        Name = "Unknown";
    }

    public Person(string name)
    {
        Name = name;
    }
}

public class Program
{
    public static void Main()
    {
        Person knownAtCompileTime = new Person("Ada");

        Type selectedType = typeof(Person);
        object activated = Activator.CreateInstance(selectedType);

        ConstructorInfo constructor = selectedType.GetConstructor(new[] { typeof(string) });
        object invoked = constructor.Invoke(new object[] { "Grace" });
        Person withArguments = (Person)invoked;

        Console.WriteLine(knownAtCompileTime.Name);
        Console.WriteLine(((Person)activated).Name);
        Console.WriteLine(withArguments.Name);
    }
}

无参数的 Activator.CreateInstance(Type) 重载要求类型具有公共无参数构造函数;它不会任意挑选一个构造函数,也不会推断参数。示例之所以可行,是因为 Person 类型定义了公共无参数构造函数;激活并创建实例时,该构造函数会将 Name 初始化为 "Unknown"。返回值的静态类型是 object;使用类专有成员前,请先将它转换为预期的类类型。在实际程序中,Type 可以来自配置或其他运行时决策,但代码仍需明确预期接收哪种对象。

使用 Type.GetConstructor 传递构造函数参数

当所需构造函数带有参数,或需要根据参数类型从多个构造函数中进行选择时,可以使用反射。Type.GetConstructor(new[] { typeof(string) }) 会查找参数列表中包含一个 string 的构造函数。查找结果是一个 ConstructorInfo;调用 Invoke(new object[] { "Grace" }) 会将相应参数传给该构造函数。随后,前面的示例会将返回对象转换为 Person,并读取已初始化的 Name。

参数类型列表是查找条件的一部分:即使两个构造函数都只接受一个参数,接受 int 的构造函数也不同于接受 string 的构造函数。当类型或签名需要在运行时确定时,反射很有用,但它会将部分检查从编译时推迟到运行时。请确认所需签名确实存在,并确保每个参数的值都与之兼容;否则,查找或调用无法创建预期的实例。如果类型和签名在编译时已知,优先使用 new,因为编译器可以直接验证调用。

仅在特殊序列化场景中创建未初始化对象

未初始化对象是在不运行其实例构造函数的情况下创建的。旧 API 名称 FormatterServices.GetUninitializedObject 描述的正是这种绕过构造函数的操作;但本文不假定该 API 在每一种 C# 运行时中都可用或受支持。不要把绕过构造函数当作一般对象创建的捷径,也不要用它来绕过私有构造函数。

构造函数代码可能会建立不变量、分配资源,或初始化后续方法默认有效的字段。跳过构造函数可能使对象处于该类未设计处理的状态。只有生命周期有明确文档说明的特殊序列化或基础设施代码,才应考虑这种模式;在正常使用对象之前,必须恢复并验证其所需状态。一般应用程序代码应使用 new、Activator.CreateInstance,或选定的 ConstructorInfo,以执行正常初始化。

选择合适的实例创建方式

对于编译时已知的类型和静态已知的构造函数,请使用 new。如果要通过运行时取得的 Type 调用可访问的无参数构造函数,请使用 Activator.CreateInstance。如果运行时构造对象时需要指定签名和参数,请使用 Type.GetConstructor 与 ConstructorInfo.Invoke。对于每种反射调用方式,都应在运行时数据进入程序的边界处检查类型和构造函数假设,然后将返回对象转换为其余代码所需的类型。

通过正常构造路径创建对象时,这些方式都会运行构造函数。未初始化实例则是另一种特殊技术,它会跳过正常路径,因此需要额外负责管理对象状态。明确这一区别,有助于根据现有类型信息选择最简单合适的构造方式。