【问题标题】:Static field initializer not being run before instance constructor静态字段初始化程序未在实例构造函数之前运行
【发布时间】:2019-02-14 14:37:42
【问题描述】:

我有以下课程:

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") 创建此类的实例时,NullReferenceException 会在实例构造函数的if 检查中抛出。调试表明ValidStatusCodesnull

ValidStatusCodes 上有一个静态初始化器。

根据 C# 规范:

如果类中存在静态构造函数(第 10.12 节), 静态字段初始化器的执行发生在紧接之前 执行该静态构造函数。 否则,静态字段 初始化器在依赖于实现的时间执行 第一次使用该类的静态字段

为什么我的静态字段在构造函数中被访问之前没有被初始化?我有一种感觉,我正在掩饰一些非常简单的事情,但是我花了太多时间调试它而没有任何进展;是时候寻求帮助了。


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

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

感谢大家的帮助。

【问题讨论】:

  • public static AssignmentStatusCode Pending ... 之前移动 private static readonly HashSet&lt;string&gt; ValidStatusCodes ... 应该可以工作。
  • 要解释为什么 Alessandro 的答案会起作用,请参阅 C# 规范 (ECMA-334) 17.11:“如果一个类包含任何带有初始化器的静态字段,那么这些初始化器会在执行静态之前立即以文本顺序执行构造函数。”
  • 啊,谢谢。我很久以前写过这个类,忘记了静态属性也通过构造函数。今天早上添加静态字段时,我也没有注意到它。这就是我错过的简单事情。
  • 我认为您在这里没有遗漏任何内容。您只有一个静态字段。引用的文档文本没有告诉我们任何关于静态属性的信息。

标签: c#


【解决方案1】:

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

C# Spec

引用:

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

也是为了完整性:

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

有人在 cmets 中正确指出,所以这没有提到属性,只是字段。我会说自动属性是私有字段和具有 get 和 set 访问器的属性的语法糖,这只是更多的糖,因此具有 Get() 和 Set(value) 方法。所以上面的内容确实适用于属性,唯一需要注意的是我不知道编译器在哪里以及如何对这些支持字段进行排序。

您使用 ctor 初始化 Pending、Rejected 和 Accepted 字段,该 ctor 依赖于该字段被初始化之后的字段。

要么把你的 hashset 字段放在首位,所以它首先被初始化。或者我认为更好的方法是使用静态 ctor 来初始化所有这些,这样您就可以清楚地看到每个的顺序和依赖关系变得更加清晰。还参考前面关于自动属性和编译器存储私有支持字段的注释,使用静态 ctor 并完全确信它们获得适当值设置的顺序会更加相关

【讨论】:

  • 来源? “静态字段和属性按照它们在类中出现的顺序进行初始化。”
  • 我猜一个属性的初始化程序被编译为 IL 作为其支持字段的初始化程序,这意味着规范在技术上是正确的。
  • @BradleyUffner 技术上正确是最好的正确方式,但我将编辑并包含一些感觉合适的来源
  • 对于我关于初始化器进入支持字段的猜测:来自C# spec“自动属性可以选择有一个property_initializer,它作为变量初始化器直接应用于支持字段(变量初始化器)。”
【解决方案2】:

我认为问题在于,在public static AssignmentStatusCode Pending { get; } = new AssignmentStatusCode("P"); 行上,您的代码包含对实例构造函数的调用,该构造函数在 HashSet 初始化之前执行。

我认为您收到的异常的堆栈跟踪应该表明构造函数的调用引发错误是我们指向的那个。

按照评论中的建议,您应该将 private static readonly HashSet&lt;string&gt; ValidStatusCodes = new HashSet&lt;string&gt;(new[] { "A", "R", "P" }); 移到构造函数调用的行之前。

【讨论】:

    【解决方案3】:

    您的属性在根据您订购它们的顺序实例化您的类时被初始化。 ** ValidStatusCodes ** 假设在你的类的顶部,你有这个错误的原因是你在初始化你的 ValidStatusCodes 属性之前调用了构造函数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-01-07
      • 2014-08-25
      • 2011-02-15
      • 1970-01-01
      • 2014-02-25
      • 1970-01-01
      • 2015-09-15
      • 1970-01-01
      相关资源
      最近更新 更多