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

29

在开发一个 C# 应用时,我注意到在多个地方静态初始化程序存在彼此的依赖关系,就像这样:

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

没有特别做什么就成功了。那只是运气吗?C# 有解决这个问题的规则吗?

编辑:(关于 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的空引用。 - Panos
现在,在同一类(局部类)的文件之间,可能由预处理器确定是否成功。 - Panos
那么如果A引用了B.b,那么初始化A.a会使B.b增加? - BCS
4个回答

23

请查看C#规范的第10.4节了解此处的规则:

当类被初始化时,该类中的所有静态字段首先被初始化为它们的默认值,然后按文本顺序执行静态字段初始值设定项。同样地,当创建类的实例时,该实例中的所有实例字段首先被初始化为它们的默认值,然后按文本顺序执行实例字段初始值设定项。可能会观察到具有变量初始值设定项的静态字段处于其默认值状态。但是,出于风格上的原因,强烈不建议这样做。

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

这些规则与Java的规则不同(请参见JLS第8.3.2.3节,其中关于前向引用的规则更为严格)。


我问的问题得到了很好的回答。然而,似乎我没有问我想问的问题:跨文件怎么办? - BCS
我不是C#高手(只是知道在哪里找),但请查看http://msdn.microsoft.com/en-us/library/aa645612(VS.71).aspx --“可以构造循环依赖项,允许观察具有可变初始值的静态字段处于其默认值状态”。这有帮助吗? - Cowan
@Cowan 不完全是。它没有解决跨文件问题。 - BCS

15

似乎取决于代码行的顺序。以下代码可以正常工作:

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); }
我们得到了这个输出:

we are getting this output:

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.

因此,在静态构造函数 A 中初始化 B 导致异常,并使字段 a 保留了默认值(null)。由于 anullb 也无法正确初始化。

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


编辑:以防您没有阅读评论,Jon Skeet 提供了一篇非常有趣的阅读材料:静态构造函数和类型初始化器之间的区别


这是确定的。当类型首次被引用时,静态构造函数将被调用。 - Panos
2
在这里要注意静态变量初始化程序和静态构造函数之间的区别。根据是否存在静态构造函数,类型初始化发生的时间有不同的规则。请参见http://pobox.com/~skeet/csharp/beforefieldinit.html。 - Jon Skeet

2

个人建议去掉静态初始化器,因为它不够清晰,并添加静态构造函数来初始化这些变量。

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

static SomeClass()
{
    a = new List<int>() { 0 };
    b = new List<int>() { a[0] };
}

这样你就不必猜测正在发生什么,而且你的意图也会更加清晰明确。


2
请注意,这些代码片段在运行时并不完全等效:http://pobox.com/~skeet/csharp/beforefieldinit.html - Jon Skeet

0

是的,你很幸运。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] };
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接