首页 / C# 入门教程 / 反射与 Assembly

C# 入门教程

反射与 Assembly

本教程共 100 篇 · 第 80 篇 · 更新于 2026-07-31 · 约 12 分钟阅读

C#C# 入门教程反射Assembly程序集插件

80. 反射与 Assembly

本节目标:学完你会加载一个程序集、在不知道具体类型时动态 new 出对象,并读取贴在类型上的特性。

上一章会用反射”看”类型。这章更进一步:把整个程序集(assembly,即编译出的 dll/exe)加载进来,动态创建实例、读取特性。这正是插件系统和依赖注入容器的核心手法。

先打个比方:集装箱里的货物

程序集就像一个个集装箱(.dll / .exe),里面整整齐齐码着各种类型”货物”。平时你只跟自己手上的货打交道;而 Assembly 这把”叉车”能打开任意一个集装箱,把里面所有货物名单一一念出来,还能当场把某件货”造”一个出来用。这就是反射加载程序集的直觉。

程序集 Assembly 是什么

程序集是 .NET 的部署单元,一个 .dll.exe 就是一个程序集,里面装着类型与元数据。通过 Assembly 类,你能列举它包含的所有类型。

namespace CSharpDemo;

using System.Reflection;

class Person
{
    public string Name { get; set; } = "";
}

var asm = typeof(Person).Assembly;
Console.WriteLine($"程序集名:{asm.GetName().Name}");
foreach (var type in asm.GetTypes())
    Console.WriteLine($"包含类型:{type.Name}");
Note

想加载外部 dll,可用 Assembly.LoadFrom("插件.dll")。本例用当前程序集演示,避免路径依赖。

用 Activator 动态创建对象

知道 Type 后,不必在代码里写 new 具体类,而是用 Activator.CreateInstance(Type) 在运行时造对象。这叫”晚绑定(late binding)”。

namespace CSharpDemo;

class Greeter
{
    public void Say() => Console.WriteLine("你好,我是动态创建的");
}

Type t = typeof(Greeter);
object? instance = Activator.CreateInstance(t);

if (instance is Greeter g)
    g.Say();
Tip

Activator.CreateInstance 返回 object?,需要 is 或强制转换才能当具体类型用。它常配合”接口”使用:你只知道对象实现了某接口,不关心具体类。

读取特性:让标签活起来

反射不仅能看成员,还能把贴上去的特性读出来。下面定义一个 [Plugin],再在运行时扫描谁被标了它。

namespace CSharpDemo;

using System.Reflection;

[Plugin("你好插件")]
class HelloPlugin
{
    public void Run() => Console.WriteLine("插件运行中");
}

Type t = typeof(HelloPlugin);
var attr = t.GetCustomAttribute<PluginAttribute>();
if (attr is not null)
    Console.WriteLine($"发现插件:{attr.Name}");

class PluginAttribute : Attribute
{
    public string Name { get; }
    public PluginAttribute(string name) => Name = name;
}
Note

GetCustomAttribute<T>() 找不到时返回 null,所以要用 is not null 判断。特性这下从”静态标签”变成了”可据以行动的开关”。

综合:扫描程序集里的所有插件

把”加载程序集 + 遍历类型 + 读特性 + 动态创建”串起来,就是一个迷你插件框架的骨架。

namespace CSharpDemo;

using System.Reflection;

[Plugin("问候插件")]
class HelloPlugin : IPlugin
{
    public void Run() => Console.WriteLine("Hello from plugin");
}

var asm = Assembly.GetExecutingAssembly();
foreach (var type in asm.GetTypes())
{
    if (type.GetCustomAttribute<PluginAttribute>() is { } pluginAttr
        && typeof(IPlugin).IsAssignableFrom(type))
    {
        Console.WriteLine($"加载插件:{pluginAttr.Name}");
        var instance = (IPlugin?)Activator.CreateInstance(type);
        instance?.Run();
    }
}

interface IPlugin
{
    void Run();
}

class PluginAttribute : Attribute
{
    public string Name { get; }
    public PluginAttribute(string name) => Name = name;
}

插件化思路(简述)

真实项目里,这套机制让你做到:主程序不动,把新功能做成独立的 dll 丢进插件目录,启动时扫描加载。好处是”开后门”式扩展,新增能力不用重新编译主程序。代价是反射有性能开销、且类型错误要运行时才暴露,所以一般只在确需灵活的边界使用。

Tip

初学不必自己造插件系统。理解这套”加载→扫描→读特性→创建”的链路,能帮你读懂 ASP.NET、依赖注入容器等框架的底层,已经很有价值。

加载外部程序集

前面用当前程序集演示。真实插件场景是从磁盘加载一个 dll:

namespace CSharpDemo;

using System.Reflection;

// 加载同目录下的插件程序集(示意路径)
Assembly pluginAsm = Assembly.LoadFrom("MyPlugin.dll");
foreach (var type in pluginAsm.GetTypes())
    Console.WriteLine($"插件类型:{type.FullName}");
Note

加载外部 dll 涉及信任与版本兼容,生产环境要校验来源、处理版本冲突。新手先理解机制,再谈安全落地。

配合反射调用方法

拿到类型和方法后,用 Invoke 在不知道具体类的情况下执行逻辑——和上一章的调用方式一脉相承。

namespace CSharpDemo;

using System.Reflection;

class Greeter
{
    public void Say(string who) => Console.WriteLine($"你好,{who}");
}

Type t = typeof(Greeter);
object obj = Activator.CreateInstance(t)!;
t.GetMethod("Say")!.Invoke(obj, new object[] { "世界" });

泛型类型的反射

反射同样能处理泛型。typeof(List<int>) 拿到的是”已关闭”的泛型类型;想拿”开放”的 List<>,可写 typeof(List<>),这在写通用框架时很有用。

namespace CSharpDemo;

Type closed = typeof(List<int>);
Type open = typeof(List<>);
Console.WriteLine($"闭合:{closed.Name}");
Console.WriteLine($"开放:{open.Name}");

常见坑与注意事项

Warning

Activator.CreateInstance(type) 只能调用”无参构造函数”。若目标类型只有带参构造,会抛 MissingMethodException。需要传参时,用 CreateInstance(type, arg1, arg2) 的重载。

Warning

加载外部 dll(Assembly.LoadFrom)要警惕版本冲突与不可信代码:版本不对会运行时崩溃;来源不明等于把未知代码请进进程。生产环境务必校验来源、锁定版本。

其它易错点:

  1. GetTypes 可能抛 ReflectionTypeLoadException:程序集里若有加载失败的类型,整个 GetTypes() 会抛异常。健壮代码应捕获它并从 Exceptions 里取出成功加载的部分。
  2. 忘记 IsAssignableFrom:扫描插件时直接 CreateInstance 却不检查是否实现接口,造出的对象可能根本调不了你的方法。先 typeof(IPlugin).IsAssignableFrom(type) 兜底。
  3. 滥用反射做高频逻辑:每次都 GetTypes + 遍历很慢,插件目录应在启动时扫一次并缓存结果,别在每个请求里重扫。

安全与性能提醒

反射强大也危险:它能绕过访问修饰符(私有也能读)、性能偏低,且类型错误要运行时才暴露。因此:

Tip

只在”编译期无法确定类型”的边界使用反射;加载外部代码前校验来源;高频调用考虑缓存 Type / MethodInfo,避免反复查找。

从目录批量加载插件(思路)

真实插件系统常把 dll 丢进一个 plugins 文件夹,启动时逐个加载、扫描。下面给出骨架(路径仅为示意):

namespace CSharpDemo;

using System.Reflection;

string pluginDir = "plugins";
if (Directory.Exists(pluginDir))
{
    foreach (string dll in Directory.GetFiles(pluginDir, "*.dll"))
    {
        try
        {
            Assembly asm = Assembly.LoadFrom(dll);
            foreach (var type in asm.GetTypes())
            {
                if (typeof(IPlugin).IsAssignableFrom(type))
                {
                    var instance = (IPlugin?)Activator.CreateInstance(type);
                    instance?.Run();
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"加载 {dll} 失败:{ex.Message}");
        }
    }
}

interface IPlugin
{
    void Run();
}
Warning

扫描外部目录加载代码,等于”运行别人放进来的程序”。生产环境必须:限定目录、校验文件来源与签名、用独立上下文隔离,绝不能直接 LoadFrom 来历不明的 dll。

读特性后做”配置化”动作

把”读特性”再推一步:框架扫描到 [Plugin] 后,不只要运行,还能读出特性里的元数据(名称、版本、优先级),据此决定加载顺序或是否启用。这就是”元数据驱动行为”的精髓——逻辑写在标签里,主程序只负责读标签、按标签办事。

Tip

你现在应该能读懂不少框架的”魔法”了:ASP.NET 的路由特性、单元测试的 [Fact]、DI 容器按接口注入实现——底层都是”扫描程序集 + 读特性 + 反射创建”这套组合拳。

配置驱动的思路雏形

把”类型名写在配置文件里,运行时反射加载”组合起来,就是配置驱动(config-driven)的雏形:改配置就能换实现,不必改代码重编译。很多框架的”按配置装配”正是这套思路。

Note

学完反射,你等于拿到了”看穿并操控任意类型”的钥匙。它不该天天用,但一旦遇到序列化、插件、框架扩展,它就是绕不开的底层能力。

小结

程序集是类型的容器,用 Assembly.GetTypes() 可枚举类型;Activator.CreateInstance(Type) 在运行时造对象;GetCustomAttribute<T>() 把特性读成可行动的数据。三者结合就是插件与 DI 容器的雏形。到这一章,C# 语言核心的”高级探照灯”——反射,你已初步掌握。


本教程第 71–80 章到此告一段落:从 LINQ 聚合与关联,到异常三板斧,再到特性与反射。它们共同构成了用 C# 写”健壮、可扩展”程序的关键工具箱。