---
URL: "/zh-CN/blog/2026-06/ModuleInitializer.html"
LLMS_URL: "/zh-CN/blog/2026-06/ModuleInitializer.md"
lastUpdated: true
commentabled: true
recommended: true
title: "C# ModuleInitializer"
description: "程序集级别的初始化黑科技"
date:

pageClass: "blog-page-class"
cover: "/covers/dotnet.svg"
---

---
lastUpdated: true
commentabled: true
recommended: true
title: C# ModuleInitializer
description: 程序集级别的初始化黑科技
date: 2026-06-02 13:35:00
pageClass: blog-page-class
cover: /covers/dotnet.svg
---

## 🤔 你是否遇到过这些困境？ ##

在实际项目里，有一类问题几乎每个 C# 开发者都踩过坑——程序集加载时需要做一些全局性的初始化工作，但你不知道该把这段逻辑放在哪里。

比如注册全局的序列化器、初始化日志框架、预热缓存、设置默认编码……这些事情必须在任何业务代码执行之前完成，但又没有一个"天然"的入口。于是你开始在 `Main()` 里堆代码，或者在每个类的静态构造函数里写初始化，结果维护成本越来越高，初始化顺序也越来越难以控制。

C# 9.0 引入的 `[ModuleInitializer]` 特性，正是为了解决这个问题而生的。它提供了一种在程序集层面、任何用户代码执行之前自动触发初始化的机制，干净、优雅、无侵入。

读完本文，你将掌握：

- ModuleInitializer 的底层运行机制

- 它与静态构造函数、AppDomain 事件的本质区别

- 3 个可直接落地的实战场景与完整代码

## 1️⃣ 问题深度剖析：初始化的"无主之地" ##

### 传统方案的痛点 ###

在 `[ModuleInitializer]` 出现之前，C# 开发者通常有以下几种选择来处理全局初始化：

#### 方案一：在 `Main()` 中集中初始化 ####

```cs
static void Main(string[] args)
{
    // 各种初始化逻辑堆在这里
    LogManager.Initialize();
    SerializerRegistry.RegisterDefaults();
    CacheWarmup.Run();
    // ... 然后才是真正的业务逻辑
}
```

这种方式最直接，但问题也最明显—— *它只适用于有 `Main()` 的可执行程序*。类库项目根本没有入口点，你无法强制库的使用者在调用你的 API 之前先执行某段初始化代码。

#### 方案二：静态构造函数（Static Constructor） ####

静态构造函数的触发时机是"*第一次访问该类型时*"，这意味着它是懒触发的，无法保证在程序集加载后立即执行。如果初始化逻辑涉及跨类型的依赖，顺序就很难控制，线程安全问题也随之而来。

#### 方案三：约定俗成的"Init"方法 ####

这是最脆弱的方案。一旦使用者忘记调用，程序可能在运行时才出现莫名其妙的错误，排查成本极高。

这三种方案都有一个共同的缺陷：*初始化逻辑与调用者耦合，或者依赖运行时的某个特定时机*，缺乏一种真正意义上"程序集自治"的初始化能力。

## 2️⃣ 核心要点提炼：ModuleInitializer 的底层机制 ##

### 什么是 Module？ ###

要理解 ModuleInitializer，首先要明白 .NET 里"模块（Module）"的概念。在 .NET 的 PE 文件结构中，一个程序集（Assembly）可以包含一个或多个模块（Module），通常情况下一个程序集就是一个模块。模块是 IL 代码的物理载体。

在 IL 层面，每个模块都可以有一个特殊的方法叫做 `.cctor`（模块级静态构造函数，也称 Module Initializer）。这个方法由 CLR 在模块被加载时自动调用，早于任何其他代码。

C# 9.0 的 `[ModuleInitializer]` 特性，正是将这个底层的 IL 机制暴露给了 C# 开发者。

### 使用规则与约束 ###

[ModuleInitializer] 的使用有明确的编译器约束，必须满足以下条件：

- 方法必须是 static

- 方法必须没有参数，也没有返回值（void）

- 方法不能是泛型方法，也不能包含在泛型类中

- 方法必须可以从模块内部访问（internal 或 public 均可）

- 不能是 extern 方法

### 执行时机与顺序 ###

`[ModuleInitializer]` 的执行时机非常早，具体顺序如下：如果同一个程序集中存在多个 `[ModuleInitializer]` 方法，它们的执行顺序由编译器决定（通常按照源码中的定义顺序），不建议依赖多个初始化器之间的执行顺序。

## 3️⃣ 解决方案设计：三个落地场景 ##

### 🚀 场景一：类库的自动注册机制 ###

这是 `[ModuleInitializer]` 最典型的使用场景。假设你在开发一个序列化类库，希望在库被引用时自动注册默认的转换器，而不需要使用者手动调用任何初始化方法。

使用者引用这个库后，无需调用任何初始化方法，SerializerRegistry 就已经处于就绪状态。这种"零配置"体验在框架和基础库开发中非常有价值。

### 🔧 场景二：Source Generator 与代码生成配合 ###

`[ModuleInitializer]` 与 Source Generator 的结合是一个非常强大的组合。Source Generator 可以在编译期扫描所有标记了某个特性的类型，然后生成一个 `[ModuleInitializer]` 方法来自动完成注册，彻底消除运行时反射扫描的开销。

### 🛡️ 场景三：诊断与环境验证 ###

在一些对运行环境有严格要求的应用中（如工控软件、金融系统），可以用 `[ModuleInitializer]` 在程序集加载时立即进行环境检查，确保问题在最早的时机被发现，而不是等到某个功能被调用时才爆出难以定位的异常。

这种"快速失败（Fail Fast）"的设计哲学，能将潜在的配置问题从"运行时某个随机时刻"提前到"程序集加载时"，极大降低了问题排查的难度。

## ⚠️ 踩坑预警：这些地方要小心 ##

### 陷阱一：初始化器中抛出未处理异常 ###

`[ModuleInitializer]` 中抛出的异常会导致 TypeInitializationException 或直接的程序崩溃，且错误信息可能不够直观。建议在初始化器内部做好异常捕获，将致命错误与可恢复的警告分开处理。

### 陷阱二：在初始化器中访问未就绪的类型 ###

由于 `[ModuleInitializer]` 执行时各类型的静态构造函数可能尚未运行，不要在初始化器中假设某个类型已经完成了自己的静态初始化。应尽量保持初始化器的逻辑简单、自包含。

### 陷阱三：测试项目的干扰 ###

在单元测试项目中，如果测试程序集引用了包含 `[ModuleInitializer]` 的程序集，初始化器会在测试运行前自动触发。这通常是期望行为，但如果初始化器依赖外部资源（如数据库连接），可能导致测试环境下的意外失败。建议通过环境变量或编译条件来区分测试环境。

## 💡 三句话总结 ##

- `[ModuleInitializer]` 是 CLR 模块级 `.ctor` 的 C# 语法糖，执行时机早于任何用户代码，是类库实现"零配置自动初始化"的最优解。

- 它不是静态构造函数的替代品，而是填补了"程序集加载时"这个初始化时机的空白，两者在触发时机和适用场景上有本质区别。

- 与 Source Generator 结合使用时威力倍增，可以在编译期完成类型扫描与注册，彻底消除启动时的反射开销。

## 💬 互动话题 ##

在你的项目中，程序集级别的初始化逻辑通常是怎么处理的？是集中在 `Main()` 里、依赖 DI 容器的启动流程，还是已经在尝试 [ModuleInitializer]？

另外，如果你在类库开发中遇到过"使用者忘记调用初始化方法"导致的 Bug，欢迎在评论区聊聊当时的场景，这类问题往往比表面看起来更有意思。
