在 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. 使用LazyT实现延迟且线程安全的初始化LazyT是个很贴心的工具它能把实例的创建推迟到真正需要的时候而且默认是线程安全的static readonly LazyMyLogger 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.