静态字段初始化程序在实例构造函数之前未被运行

5

我有以下类:

public class AssignmentStatusCode 
{

    public static AssignmentStatusCode Pending { get; } = new AssignmentStatusCode("P");

    public static AssignmentStatusCode Rejected { get; } = new AssignmentStatusCode("R");

    public static AssignmentStatusCode Approved { get; } = new AssignmentStatusCode("A");


    public static implicit operator string(AssignmentStatusCode assignmentStatusCode)
    {
        return assignmentStatusCode.Value;
    }

    private static readonly HashSet<string> ValidStatusCodes = new HashSet<string>(new[] { "A", "R", "P" });

    public AssignmentStatusCode(string value)
    {
        if (!ValidStatusCodes.Contains(value))
        {
            throw new ArgumentOutOfRangeException(nameof(value),
                                                  $"Value must be {string.Join(", ", ValidStatusCodes.Select(c => $"'{c}'"))}.");
        }

        Value = value;
    }

    public string Value { get; }
}

当我使用 var a = new AssignmentStatusCode("A") 创建此类的实例时,实例构造函数中if检查语句处抛出NullReferenceException异常。调试表明ValidStatusCodesnull

ValidStatusCodes具有静态初始化器。

根据C#规范:

如果类中存在静态构造函数(§10.12),则在执行该静态构造函数之前即刻执行静态字段初始化器。否则,在第一次使用该类的静态字段之前,将在实现依赖时间内执行静态字段初始化器。

为什么我的静态字段在构造函数中被访问之前未被初始化?我感觉自己错过了非常简单的东西,但我已经花了太多时间进行调试,没有任何进展;现在是请求帮助的时候了。


显然,如果我更加仔细地阅读了规范,我就会注意到我引用的段落的开头,这是我问题的根源。

10.5.5.1 静态字段初始化 类的静态字段变量初始化器对应于按照类声明中它们出现的文本顺序执行的一系列赋值。如果类中存在静态构造函数(§10.12),则在执行该静态构造函数之前即刻执行静态字段初始化器。否则,在第一次使用该类的静态字段之前,将在实现依赖时间内执行静态字段初始化器。

感谢大家的帮助。


1
private static readonly HashSet<string> ValidStatusCodes ... 移动到 public static AssignmentStatusCode Pending ... 之前,应该就可以工作了。 - Alessandro D'Andria
从C#规范(ECMA-334)17.11中可以解释为什么Alessandro的答案有效:“如果一个类包含任何具有初始值设定项的静态字段,则这些初始化器将在执行静态构造函数之前立即按文本顺序执行。” - ckuri
啊,谢谢。我很久以前写了这个类,忘记了静态属性也要通过构造函数进行初始化。今天早上我添加静态字段时也没有注意到它。这就是我错过的简单事情。 - Bradley Uffner
1
我认为你没有漏掉任何东西。你只有一个静态字段。引用的文档文本并没有告诉我们任何关于静态属性的信息。 - l33t
3个回答

4

静态字段和属性按照它们在类中出现的顺序进行初始化。

C#规范

引用如下:

一个类的静态字段变量初始化器对应于一系列赋值,这些赋值按照它们在类声明中出现的文本顺序执行。

另外为了完整性:

如果类中存在静态构造函数,则在执行该静态构造函数之前立即执行静态字段初始化器。否则,在第一次使用该类的静态字段之前的实现依赖时间执行静态字段初始化器。

有人在评论中指出并且正确地指出,这没有提到属性,只是字段。我会说自动属性是私有字段和具有get和set访问器的属性的语法糖,这只是更多的糖果,因此具有Get()和Set(value)方法。因此,上述内容适用于属性,唯一的限制是我不知道编译器在哪里以及如何排序那些支持字段。

您使用依赖于在该字段之后初始化的字段的构造函数来初始化Pending、Rejected和Accepted字段。

要么将您的哈希集字段放在第一位,这样它就会首先初始化。或者我认为更好的方法是使用静态构造函数来初始化所有内容,这样您可以清楚地看到每个依赖项的顺序和依赖关系变得更加清晰。此外,参考之前关于自动属性以及编译器存储私有后备字段的注释,更应该使用静态构造函数,并完全确信它们获得适当值设置的顺序。


再试一次!“静态字段和属性”。 - l33t
1
我猜测属性的初始化器被编译为其后备字段的初始化器,这意味着规范在技术上是正确的。 - Bradley Uffner
1
@BradleyUffner 技术上的正确是最好的正确,但我会编辑并包含一些来源,因为那样更恰当。 - Dave
1
关于我的初始化程序进入后备字段的猜测:根据C#规范,“自动属性可以选择具有property_initializer,该属性直接作为variable_initializer(变量初始化器)应用于后备字段。” - Bradley Uffner
显示剩余2条评论

3
我认为问题在于你的代码在public static AssignmentStatusCode Pending { get; } = new AssignmentStatusCode("P");这行中包含了一个实例构造函数的调用,而该调用是在HashSet初始化之前执行的。
我认为你收到的异常堆栈跟我们指出的那个构造函数调用有关。
如评论所建议的,你应该将private static readonly HashSet<string> ValidStatusCodes = new HashSet<string>(new[] { "A", "R", "P" });移动到构造函数调用的那一行之前。
注意:请勿修改原始代码中的HTML标签。

0

在实例化类时,您的属性会按照您订购它们的顺序进行初始化。** ValidStatusCodes ** 应该位于您的类顶部,您出现这个错误的原因是在初始化 ValidStatusCodes 属性之前调用了构造函数。


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