我的 C# 程序没有根据对象初始化顺序按照我认为的方式初始化对象。为什么?

My C# program does not initialize the Object how I think it has to according to the Object initialization order. Why?

我相信 C# 的对象初始化顺序是这样的:

下面是一个简单的测试程序以及当我 运行 它时它产生的输出。

    public class UiBase
    {
        protected static string Something = "Hello";

        public UiBase()
        {
            Console.WriteLine(this.ToString());
        }
    }

    public class Point : UiBase
    {
        private int X = -1;
        private int Y = -1;

        static Point()
        {
            Something = "Bar";
        }

        public Point(int x, int y)
        {
            X = x;
            Y = y;
        }

        public override string ToString()
        {
            return $"Point:{X}/{Y}/{Something}";
        }
    }

    public static class Program{
    public static void Main(){
        var x = new Point(2,1);
        Console.WriteLine(x);
    }
on Console:
Point:-1/-1/Bar
Point:2/1/Bar

当我根据上面的列表思考它应该如何发生时,我认为它应该是这样的:

  1. 点静态字段(none 在我的例子中?)
  2. 点静态构造函数 -> 将 Something 设置为 "Bar"
  3. 点实例字段
  4. 基本静态字段 -> 将 Something 设置回 "Hello"? ...

但是它并没有将 Something 设置回 Hello,这让我很困惑。那么我该如何解释呢?还是对象初始化与我所说的不同?

你在基础 UiBase class 构造函数中调用了一个虚拟成员 ToString()

Console.WriteLine(this.ToString());

它在 Point 构造函数

之前被调用
public Point(int x, int y)
{
     X = x;
     Y = y;
}

this 尚未完全初始化,您将在输出中得到 -1。由于 ToString() 是虚拟方法,因此根据 specs

调用 Point.ToString()

The overriding member in the most derived class is called, which might be the original member, if no derived class has overridden the member.

在创建 Point 的实例或引用任何静态成员之前自动调用静态构造函数(有关详细信息,请查看 static constructors

static Point()
{
     Something = "Bar";
}

它将从基数 class 覆盖 Something,并且在这两种情况下您都会在输出中得到 BarSomething 永远不会设置回 Hello,它只会被覆盖一次。

Something 字段完全针对 UiBasePoint class 中没有副本,它的值将随处更改。根据 static members

Only one copy of a static member exists, regardless of how many instances of the class are created.

如果您在 Console.WriteLine(x); 之后打印 UiBase.Something,您将得到 Bar,而不是 Hello。对于通用 classes 只有一个例外,但它超出了您的问题范围。

在执行顺序方面,所有字段初始值设定项 运行 按从派生 class 到基的顺序,然后所有构造函数 运行 按从基到派生的顺序(这是正确的实例成员)。我为您的所有操作添加了一个步骤,以查看实际订单。

public class UiBase
{
    private static int temp = Step("uibase static field init");
    public static string Something = "Hello";

    private int _temp = Step("uibase instance field init");

    public static int Step(string message)
    {
        Console.WriteLine(message);
        return 0;
    }

    public UiBase()
    {
        Step("uibase instance ctor");
        Console.WriteLine(this.ToString());
    }
}

public class Point : UiBase
{
    private int _temp = Step("point instance field init");

    private int X = -1;
    private int Y = -1;

    static Point()
    {
        Step("point static ctor before");
        Something = "Bar";
        Step("point static ctor after");
    }

    public Point(int x, int y)
    {
        Step("point instance ctor");

        X = x;
        Y = y;
    }

    public override string ToString()
    {
        return $"Point:{X}/{Y}/{Something}";
    }
}

输出如下

point static ctor before
uibase static field init
point static ctor after
point instance field init
uibase instance field init
uibase instance ctor
Point:-1/-1/Bar
point instance ctor
Point:2/1/Bar

先调用Point静态构造函数(Pointclass中没有静态字段),然后'ask'UiBaseinit一个静态字段,因为访问它的 Something 值(它被设置为 Hello),之后 Something 被设置为 Bar 并继续执行实例初始化(再次,Something 永远不会改变) - 派生 class 字段,基础 class 字段,基础 class 构造函数和派生 class 构造函数。

我认为,只有前 3 行可能有点混乱,但静态初始化只发生一次,并且在任何实例初始化之前发生。静态初始化的顺序由编译器根据你的实际代码决定。

添加一个UiBase静态构造函数其实可以让画面更清晰,在这种情况下UiBase静态成员将在Point静态初始化之前被初始化。