当前位置: 首页 > news >正文

C#静态构造函数真的总是最先执行吗?

在 C# 开发圈子里,有一个流传很广的说法,甚至经常被当成面试题:“当第一次访问某个类型时,该类型的静态构造函数一定会最先执行。”听起来好像挺有道理,但严格来说,这个说法并不完全准确。根据 ECMA-335 CLI 规范(Common Language Infrastructure Specification),静态字段初始化器(static field initializers)的执行顺序其实是排在静态构造函数体(static constructor body)之前的。换句话说,在某些特殊情况下,实例构造函数反而可能在静态构造函数体之前被调用①。这个细节如果没搞清楚,很容易写出让人摸不着头脑的 bug。


一个最小化示例

来看一段简单的代码:

class MyLogger { static MyLogger inner = new MyLogger(); // 静态字段初始化器 static MyLogger() { Console.WriteLine("Static"); } private MyLogger() { Console.WriteLine("Instance"); } }

很多人第一反应会觉得输出应该是:

Static Instance

但实际运行结果却是:

Instance Static

为什么会这样?因为static MyLogger inner = new MyLogger();属于静态字段初始化器,而它在类型初始化过程中比静态构造函数体执行得更早。也就是说,在执行这行代码时,会先调用实例构造函数new MyLogger(),打印“Instance”,然后才轮到静态构造函数体中的Console.WriteLine("Static")


规范定义的执行顺序

CLI 规范对类型初始化的顺序有非常明确的说明②:

首先,所有静态字段初始化器会按照它们在代码中出现的先后顺序依次执行;然后,才会执行静态构造函数体里的代码。所以,如果某个静态字段初始化器里创建了当前类型的实例,那么实例构造函数必然会在静态构造函数体之前被触发。这并不是编译器的 bug,而是规范定义好的行为。


为何这会引发问题?

这种执行顺序最坑的地方在于,如果实例构造函数依赖于静态成员,就很容易出现空引用异常。比如下面这个例子:

class Service { staticstring ConnectionString; static Service Instance = new Service(); // 实例构造函数在这里调用 static Service() { ConnectionString = LoadFromConfig(); // 这一行还没执行 } private Service() { // 此时 ConnectionString 还是 null! Console.WriteLine(ConnectionString.Length); // 抛出 NullReferenceException } }

在这个场景里,Instance = new Service()在静态构造函数体给ConnectionString赋值之前就被执行了,导致实例构造函数拿到的是未初始化的ConnectionString,一用就崩。


如何正确处理?

既然知道了这个坑,我们可以通过几种方式来避开它。

1. 避免在静态字段初始化器中创建依赖静态状态的实例

最简单的办法就是把实例的创建挪到静态构造函数内部,确保所有静态依赖都先初始化好:

class MyLogger { static MyLogger inner; static MyLogger() { // 先完成所有静态设置 inner = new MyLogger(); Console.WriteLine("Static"); } private MyLogger() { Console.WriteLine("Instance"); } }
2. 使用Lazy<T>实现延迟且线程安全的初始化

Lazy<T>是个很贴心的工具,它能把实例的创建推迟到真正需要的时候,而且默认是线程安全的:

static readonly Lazy<MyLogger> inner = new(() => new MyLogger()); public static MyLogger Inner => inner.Value;

这样,只有在第一次访问Inner属性时才会创建实例,彻底避开了类型初始化阶段的依赖问题。

3. 减少构造函数中的副作用

一个更根本的思路是,尽量让构造函数(无论是实例还是静态)保持简单,不要在里面读取配置、调用服务定位器或依赖注入容器。这些操作越少,因为初始化顺序导致的问题就越少,代码也更容易理解和调试。


核心结论

“静态构造函数总是最先执行”这句话其实是一种过度简化,很容易误导人。更准确的理解应该是:

  • 当一个类型第一次被使用时,会触发它的类型初始化过程;

  • 在这个过程中,静态字段初始化器会先执行(按声明顺序);

  • 然后才轮到静态构造函数体中的代码。

正因为这样,像static Foo x = new Foo();这样的写法,会先调用实例构造函数,再执行静态构造函数体。只有掌握了这个机制,才能写出更健壮的代码,避免那些藏在初始化顺序里的隐蔽 bug③。


参考资料

① ECMA International.ECMA-335: Common Language Infrastructure (CLI) Partitions I to VI. 6th Edition, June 2012. https://www.ecma-international.org/publications-and-standards/standards/ecma-335/
② Microsoft.Static Constructors (C# Programming Guide). https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors
③ Jon Skeet.C# in Depth, 4th Edition. Manning Publications, 2019.

http://www.jsqmd.com/news/440460/

相关文章:

  • 每日一题:什么是限流?.NET 中如何实现?
  • 前后端交互中时间的格式化与解析,将会面临哪些问题?
  • yolo go onnx
  • 2026.3.5总结 安装claude code 并在vscode上调用
  • gcsfuse中的锁与偏序理论
  • 大模型训练的硬件基础:GPU内存层级、分块与并行策略
  • 2026新春零食囤货推荐:《旺旺大礼包》种类多性价比高的新年限定年味零食大礼包 - Top品牌推荐官
  • 2026全国最新纯磷虾油品牌推荐 - 十大品牌榜
  • 在云主机上安装openclaw
  • 笔耕不辍,聊聊 7 种实现异步编程的方式
  • 静态链接程序的执行流程分析
  • “政务场景AI落地”并非替代人力,而是通过技术赋能,让政务工作者更专注于需要判断力、共情力与协调力的核心职责
  • Agentic AI提示工程设计的关键性能指标:架构师该关注哪些?
  • 2026转行秘籍:成为大模型产品经理的全面指南,AI产品经理=大模型产品经理?
  • 32 图 | 玩转 Spring Cloud Gateway + JWT 登录认证
  • 拆解一款零数据上传的在线工具箱:前端实现与工程化思路
  • 为什么 mysql 的 count() 方法这么慢?找到内鬼了
  • 2026全国最新进口磷虾油品牌推荐:适配多维健康需求,这款实力之选值得关注 - 十大品牌榜
  • CMake 最小可跑实战:从 0 构建第一个 C++ 可执行程序(C++ 工程入门第二课)
  • 2026年全国南极磷虾油品牌优选指南 四大品质品牌参考 - 十大品牌榜
  • 奇淫巧技,CompletableFuture 异步多线程是真的优雅
  • 遍历需要取字符串 / 数组下标
  • 支付宝消费券回收价格历史最高多少? - 京顺回收
  • 给分库分表的 ShardingSphere 提了个PR,这Bug居然改了
  • 计算机
  • 分库分表后如何设计索引?全局索引、二级索引
  • SpringCloud + RocketMQ 实现分布式事务,稳的一批
  • LoRA爆了?这篇论文硬核打脸!纯LoRA知识库路线要凉?真相竟是它…(附实验证明)
  • AI大模型卷向超长上下文:从参数规模到上下文长度,谁才是AI智能的关键?
  • OpenClaw火爆出圈!246K星!硬核拆解本地化AI助理架构,企业级Agent架构演进至17层!