简体   繁体   English

C# - 从其他类的 static 构造函数访问字段时,不调用 Static 初始化程序

[英]C# - Static initializer is not called when field is accessed from other class' static constructor

I have the following struct我有以下结构

public struct RenderLayer : IComparable<RenderLayer>, IEqualityComparer<RenderLayer> {

    public static readonly RenderLayer Default = new(nameof(Default), RenderLayerManager.NextLayerId);
    
    public readonly string name;
    public readonly int id;
    
    public RenderLayer (string name, int id) {
        this.name = name;
        this.id = id;
        Log.Notice("Render layer constructor");
    }

    public int CompareTo (RenderLayer other) => id.CompareTo(other.id);

    public bool Equals (RenderLayer x, RenderLayer y) {
        return x.id == y.id;
    }

    public int GetHashCode (RenderLayer obj) {
        return obj.id;
    }

}

And a manager class that holds all RenderLayers (the default one added here + others added from user configuration)还有一个管理器 class 保存所有 RenderLayers(此处添加的默认层 + 从用户配置中添加的其他层)

public static class RenderLayerManager {

    public static int NextLayerId => nextLayerId++;
    
    private static int nextLayerId = 0;
    private static readonly List<RenderLayer> layers = new();

    static RenderLayerManager () {
        Log.Notice("Render layer manager constructor, before");
        layers.Add(RenderLayer.Default);
        Log.Notice("Render layer manager constructor, after");
    }

    public static Span<RenderLayer> Layers => CollectionsMarshal.AsSpan(layers);

}

The problem is when the Layers property is used it somehow contains one RenderLayer that is not initialized.问题是当使用 Layers 属性时,它以某种方式包含一个未初始化的 RenderLayer。 The output of the Log.Notice calls is: Log.Notice 调用的 output 是:

[Notice] Render layer manager constructor, before
[Notice] Render layer manager constructor, after
[Notice] Render layer constructor

As far as I understand static initialization the static field in RenderLayer should be initialized when it is first accessed (or before).据我了解 static 初始化 RenderLayer 中的 static 字段应在首次访问时(或之前)进行初始化。 But clearly it is being initialized after the RenderLayerManager's constructor is completed但显然它是在 RenderLayerManager 的构造函数完成后初始化的

Edit: I've made an example for it.编辑:我已经为它做了一个例子。 At first it seemed to also have the error but now I can't reproduce it.起初它似乎也有错误,但现在我无法重现它。 I have no idea why my project still has this issue but this example is running as expected.我不知道为什么我的项目仍然存在此问题,但此示例按预期运行。 https://dotnetfiddle.net/P4FJ7Q https://dotnetfiddle.net/P4FJ7Q

What Blindy meant was many things. Blindy的意思是很多东西。 Avoiding mutable static state, avoiding singletons, avoiding depending on order of static initialization -- these are one of the basic rules of not insisting on making your life harder that in must be.避免可变的 static state,避免单例,避免依赖于 static 初始化的顺序 - 这些是不坚持让你的生活更难的基本规则之一。 In the code you presented, you "break" all of those three "rules", and, unsurprisingly, static initialization order somehow has biten you.在您提供的代码中,您“破坏”了所有这三个“规则”,并且毫不奇怪,static 初始化顺序以某种方式咬住了您。

What you deduced from the log is wrong.您从日志中推断出的内容是错误的。 RenderLayer is not "clearly it is being initialized after the RenderLayerManager's constructor is completed". RenderLayer 不是“显然在 RenderLayerManager 的构造函数完成后正在初始化”。

In fact, the log you provided proves, that it was FIRST to be executed.事实上,您提供的日志证明,它是第一个被执行的。 But not first to complete.但不是先完成。

Look at this line, how does it execute?看看这一行,它是如何执行的? Precisely, please?请问准确吗?

(..) readonly RenderLayer Default = new("blah", RenderLayerManager.NextLayerId);

It calls into RenderLayer constructor, but that's not all.它调用RenderLayer构造函数,但这还不是全部。 It also passes parameters.它还传递参数。 And to be able to call the constructor, it HAS TO have the parameters' values' already computed.并且为了能够调用构造函数,它必须已经计算出参数的“值”。 Then, how is the second parameter computed?那么,第二个参数是如何计算的呢?

This means, that what has happened in your code was:这意味着,您的代码中发生的事情是:

  • compiler picked RenderLayout as the first to be initialized statically, for some reason出于某种原因,编译器选择 RenderLayout 作为第一个静态初始化的
  • RenderLayout initialization has started RenderLayout初始化已经开始
  • it tried to initialize first static field: Default它试图初始化第一个 static 字段: Default
  • it started preparing parameters' values' for constructor它开始为构造函数准备参数“值”
  • doing so, it tries to read RenderLayerManager class static property这样做,它会尝试读取RenderLayerManager class static 属性
  • RenderLayerManager was not initialized yet RenderLayerManager尚未初始化
  • RenderLayerManager initialization has started RenderLayerManager初始化已经开始
  • RenderLayerManager static nextLayerId is set to 0 (not really, but let's say it is..) RenderLayerManager static nextLayerId设置为 0(不是真的,但假设是..)
  • RenderLayerManager static nextLayerId is initialized - List is created, its ctor is called, nextLayerId is set to that new list instance RenderLayerManager static nextLayerId已初始化 - 创建List ,调用其 ctor,将nextLayerId设置为该新列表实例
  • RenderLayerManager static ctor is called RenderLayerManager static ctor 被调用
  • that ctor writes out BEGIN log line那个 ctor 写出 BEGIN 日志行
  • that ctor executes layers.add , or rather, wants那个 ctor 执行layers.add ,或者更确切地说,想要
  • that ctor prepares parameters for layers.add - accesses RenderLayer.Default该ctor为layers.add准备参数-访问RenderLayer.Default
  • RenderLayer is not initialized yet - but it is UNDER INITIALIZATION - so it's static initialization procedure IS NOT CALLED AGAIN RenderLayer尚未初始化 - 但它正在初始化 - 所以它是 static 初始化过程不再被调用
  • RenderLayer.Default is read out and contains trash, I'd expect null RenderLayer.Default被读出并包含垃圾,我希望null
  • that ctor writes out END log line那个ctor写出END日志行
  • static ctor of RenderLayerManager ends RenderLayerManager的 static ctor 结束
  • execution returns where it was, static init of RenderLayer continues preparing args for new() in RenderLayer执行返回原处, RenderLayer的 static init 继续为RenderLayer中的new()准备 args
  • RenderLayerManager.NextId is read (and getter executes, increments counter, etc)读取RenderLayerManager.NextId (并执行 getter、递增计数器等)
  • all parameters for new() are now ready new()的所有参数现在都准备好了
  • RenderLayer ctor is called, does its thing, writes out third log line调用RenderLayer ctor,做它的事,写出第三个日志行

The thing with static initialization is this - it is hard to trace. static 初始化的事情是这样的——很难追踪。 Tiny things can force picking different starting point than you'd expect.微小的事情可能会迫使您选择与您预期不同的起点。 Things can be pulled and executed in the middle if they are needed.如果需要,可以在中间拉取并执行事情。 When things are under ongoing initialization, they are not guaranteed to be coherent, and so on.当事物正在进行初始化时,不能保证它们是连贯的,等等。 That's why it's best to avoid it as much as possible and instead rely on things easier to plan out.这就是为什么最好尽可能避免它,而是依靠更容易计划的事情。

Let's drop the argument about not using static, and not using singletons, that's an independent discussion.让我们放弃关于不使用 static 和不使用单例的论点,这是一个独立的讨论。 Let's focus on static initialization.让我们重点关注 static 初始化。 Why do you use it?你为什么用它?

Maybe you wanted to have it "lazy" - if "RenderLayer" AND "RenderLayerManager" were not used, they'd be not initalized.也许你想让它“懒惰”——如果没有使用“RenderLayer”和“RenderLayerManager”,它们就不会被初始化。 Maybe you wanted to have it surely initialized before setting up before everything else.也许您希望在设置之前先对其进行初始化。 Or some other reason.或者其他什么原因。 I don't know.我不知道。 But at least for those two things, there are other ways.但至少对于这两件事,还有其他方法。

  • for lazy initialization, on first access, there's the Lazy<T> class that does exactly this对于延迟初始化,在第一次访问时,有Lazy<T> class 正是这样做的

  • for "before everything else" you can just use the good old main().对于“在其他一切之前”,您可以使用旧的 main()。 Why rely on static initialization order to be able to guess what you wanted, when you can just have it just like you have it now, and just conceptually move the RL.Default = new(..) and RLM.layers.add(RL.Default) to main() before you run the game loop?为什么要依赖 static 初始化顺序来猜测您想要什么,当您可以像现在一样拥有它时,只需在概念上移动RL.Default = new(..)RLM.layers.add(RL.Default)main()在你运行游戏循环之前? You don't have to paste that code there, just wrap these pieces in methods and then in main() you can just write RLM.Initialize(); RL.Initialize();您不必在那里粘贴该代码,只需将这些部分包装在方法中,然后在main()中您就可以编写RLM.Initialize(); RL.Initialize(); RLM.Initialize(); RL.Initialize(); or something like that, and you will instantly see why the static initialization order is important and sometimes ends up in a chicken-and-egg problem you have here.或类似的东西,你会立即明白为什么 static 初始化顺序很重要,有时会导致你在这里遇到的鸡与蛋的问题。

Then, solve the chicken and egg problem.然后,解决鸡和蛋的问题。 For example, like this:例如,像这样:

public static int Main()
{
    // lets be 'sure' that RLM is first to be init'ed
    RenderLayerManager.Initialize();
}

public struct RenderLayer : ...
{ 
    public static readonly RenderLayer Default;

    public static void Initialize()
    {
        Default = new(nameof(Default), RenderLayerManager.NextLayerId);
    }
    
    ...
    
    public RenderLayer (string name, int id) {
        ...
    }

    ...
}

public static class RenderLayerManager {
    public static void Initialize()
    {
        RenderLayer.Initialize();
        layers.Add(RenderLayer.Default);
    }
    
    ...

    static RenderLayerManager () {
        Log.Notice("Render layer manager constructor, before");
        // layers.Add(RenderLayer.Default); // ***
        Log.Notice("Render layer manager constructor, after");
    }

    ...
}

Note that leaving layers.add at the old place would probably be fine now, since RLM begins initializing first, and then would pull RL, but that would happen only after RLM's field init is completed - so counter would be already ready - but - really - we want to minimize dependency on static initialization order, and that's why I move it now to RLM.Initialize() , so any reader of this code has no problem in determinig what precedes and what follows.请注意,现在将 layers.add 留在旧位置可能会很好,因为 RLM 首先开始初始化,然后会拉 RL,但这只会在 RLM 的字段初始化完成后发生 - 所以计数器已经准备好 - 但是 - 真的- 我们希望尽量减少对 static 初始化顺序的依赖,这就是我现在将其移至RLM.Initialize()的原因,因此该代码的任何读者都可以毫无问题地确定之前和之后的内容。

Key points of my proposed changes (if I not bothed it):我提议的更改的要点(如果我不介意的话):

  • static init of RL does not touch RLM RL 的 static 初始化不接触 RLM
  • static init of RLM does not touch RL RLM 的 static 初始化不接触 RL
  • RL and RLM expose Initialize methods that do final steps that must touch the other class RL 和 RLM 公开Initialize方法,这些方法执行必须触及其他 class 的最终步骤
  • caller is responsible for calling RLM.Initialize early enough before rest of the code tries to use it caller 负责调用 RLM.Initialize 在代码的 rest 尝试使用它之前足够早

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM