C#中静态构造函数/初始化器的顺序

Posted

技术标签:

【中文标题】C#中静态构造函数/初始化器的顺序【英文标题】:Order of static constructors/initializers in C# 【发布时间】:2010-09-16 04:04:26 【问题描述】:

在开发 C# 应用程序时,我注意到在几个地方静态初始化器相互依赖,如下所示:

static private List<int> a = new List<int>()  0 ;
static private List<int> b = new List<int>()  a[0] ;

没有做任何特别有效的事情。这只是运气吗? C# 有解决此问题的规则吗?

编辑: (re: Panos) 在文件中词法顺序似乎是王道?跨文件呢?

在寻找时,我尝试了这样的循环依赖:

static private List<int> a = new List<int>()  b[0] ;
static private List<int> b = new List<int>()  a[0] ;

而且程序运行不一样(测试套装全面失败,我没有进一步查看)。

【问题讨论】:

我认为跨文件(即跨不同类)它会​​发生相同的情况。在A类的类型初始化过程中,会要求B类进行初始化,B类会找到A类的空引用。 现在,跨同一类(部分类)的文件,可能由预处理器来确定它是否失败。 所以如果 A 引用 B.b 那么 A.a 被初始化会碰撞 B.b? 【参考方案1】:

这似乎取决于行的顺序。此代码有效:

static private List<int> a = new List<int>()  1 ;
static private List<int> b = new List<int>()  a[0] ;

虽然此代码不起作用(它会抛出 NullReferenceException

static private List<int> a = new List<int>()  b[0] ;
static private List<int> b = new List<int>()  1 ;

因此,显然不存在循环依赖的规则。奇怪的是编译器没有抱怨......


编辑 - “跨文件”发生了什么?如果我们声明这两个类:

public class A 
    public static List<int> a = new List<int>()  B.b[0] ;

public class B 
    public static List<int> b = new List<int>()  A.a[0] ;

并尝试使用以下代码访问它们:

try  Console.WriteLine(B.b);  catch (Exception e)  Console.WriteLine(e.InnerException.Message.); 
try  Console.WriteLine(A.a);  catch (Exception e)  Console.WriteLine(e.InnerException.Message); 
try  Console.WriteLine(B.b);  catch (Exception e)  Console.WriteLine(e.InnerException.Message); 

我们得到这个输出:

The type initializer for 'A' threw an exception.
Object reference not set to an instance of an object.
The type initializer for 'A' threw an exception.

所以B 的初始化会导致静态构造函数A 中的异常,并将字段a 保留为默认值(null)。由于anull,所以b也不能正常初始化。

如果我们没有循环依赖,一切正常。


编辑:如果您没有阅读 cmets,Jon Skeet 提供了一个非常有趣的阅读:The differences between static constructors and type initializers。

【讨论】:

这是肯定的。第一次引用类型时调用静态构造函数。 这里要注意静态变量初始化器和静态构造器之间的区别。当基于静态构造函数的存在/不存在发生类型初始化时,有不同的规则。见pobox.com/~skeet/csharp/beforefieldinit.html【参考方案2】:

是的,你很幸运。 C# 似乎按照代码在类中出现的顺序执行代码。

static private List<int> a = new List<int>()  0 ;
static private List<int> b = new List<int>()  a[0] ;

会工作,但...

static private List<int> b = new List<int>()  a[0] ;
static private List<int> a = new List<int>()  0 ;

会失败。

我建议将所有依赖项放在一个地方,静态构造函数就是这个地方。

static MyClass()

  a = new List<int>()  0 ;
  b = new List<int>()  a[0] ;

【讨论】:

【参考方案3】:

我个人会摆脱静态初始化程序,因为它不清楚并添加一个静态构造函数来初始化这些变量。

static private List<int> a;
static private List<int> b;

static SomeClass()

    a = new List<int>()  0 ;
    b = new List<int>()  a[0] ;

这样您就不必猜测发生了什么,并且您的意图很清楚。

【讨论】:

请注意,这些代码在运行时间方面并不完全等效:pobox.com/~skeet/csharp/beforefieldinit.html【参考方案4】:

这里的规则请参见section 10.4 of the C# spec:

当一个类被初始化时,该类中的所有静态字段首先被初始化为其默认值,然后静态字段初始化器按文本顺序执行。同样,当创建一个类的实例时,该实例中的所有实例字段首先被初始化为其默认值,然后实例字段初始化程序按文本顺序执行。可以在默认值状态下观察具有变量初始值设定项的静态字段。但是,作为风格问题,强烈建议不要这样做。

换句话说,在您的示例中,“b”被初始化为其默认状态(null),因此在“a”的初始化程序中对它的引用是合法的,但会导致 NullReferenceException。

这些规则与 Java 的不同(请参阅 section 8.3.2.3 of the JLS 了解 Java 的关于前向引用的规则,这些规则更具限制性)。

【讨论】:

很好的回答我提出的问题。但是,我似乎没有问我想问的问题:跨文件呢? 我不是 C# 高手(只知道在哪里看),但请查看 msdn.microsoft.com/en-us/library/aa645612(VS.71).aspx -- “可以构造循环依赖项,允许在其内部观察到带有变量初始值设定项的静态字段默认值状态”。这有帮助吗? @Cowan 不是真的。它没有解决交叉文件问题。

以上是关于C#中静态构造函数/初始化器的顺序的主要内容,如果未能解决你的问题,请参考以下文章

实例构造函数与静态构造函数执行顺序

C#静态构造函数排序[关闭]

静态代码块构造代码块构造函数以及Java类初始化顺序

静态构造函数, 静态成员初始化/调用顺序

C#图解教程 第六章 深入理解类

C# 静态类构造函数