静态构造函数是如何工作的?

83
namespace MyNameSpace
{
    static class MyClass
    {
        static MyClass()
        {
            //Authentication process.. User needs to enter password
        }

        public static void MyMethod()
        {
            //Depends on successful completion of constructor
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass.MyMethod();
        }
    }
}

以下是我假设的顺序:

  1. 静态构造函数的开头
  2. 静态构造函数的结尾
  3. 主函数的开头
  4. MyMethod的开头
  5. 主函数的结尾

现在,如果数字4在数字2之前开始,我的代码就会出错。这种情况可能吗?


8
这是一个Java还是C#的问题?您已经添加了两个标签,我认为这两种语言的规范不同。 - ARRG
在我看来,这对两者都是一样的。但我是C#程序员。对此感到抱歉。 - om471987
4
Java没有像静态构造函数一样的东西,只有用于静态初始化的静态块。static { //做某些事情... } - deraj
2
就我个人而言,我对在静态构造函数内部进行任何形式的交互感到不舒服。我理解你的目标(使该静态类中的每个方法在允许其运行之前等待用户授权),但我真的不喜欢这种实现方式。 - Brian
@Brian:- 是的...你说得对...我只是在分析...最后我决定不使用构造函数,而是使用初始化方法。 - om471987
10个回答

223

在这里您只提了一个问题,但实际上应该问十几个问题,所以我会把所有问题都回答了。

以下是我假设的顺序:

  1. 类构造函数的开始(也称为)
  2. cctor的结束
  3. Main方法的开始
  4. MyMethod方法的开始

这个假设正确吗?

不正确。正确的顺序是:

  1. 程序(Program)的cctor的开始,如果有的话。这里没有。
  2. 程序(Program)的cctor的结束,如果有的话。这里没有。
  3. Main方法的开始
  4. MyClass的cctor的开始
  5. MyClass的cctor的结束
  6. MyClass.MyMethod的开始

如果有静态字段初始化器呢?

在某些情况下,CLR允许改变静态字段初始化器运行的顺序。详情请参见Jon的页面:

静态构造函数和类型初始化器之间的区别

一个像MyMethod这样的静态方法,在该类的cctor完成之前是否可能被调用?

是的。如果cctor本身调用了MyMethod,那么显然在cctor完成之前会调用MyMethod。

cctor没有调用MyMethod。一个像MyMethod这样的静态方法,在MyClass的cctor完成之前是否可能被调用?

是的。如果cctor使用另一个类型,该类型的cctor调用了MyMethod,那么在MyClass的cctor完成之前会调用MyMethod。

不要直接或间接调用MyMethod构造函数!那么像MyMethod这样的静态方法在MyClass完成构造函数之前被调用的可能性是否存在?

不存在。

即使有多个线程参与,这个结论仍然成立。

无论有多少个线程参与,构造函数最多只会被调用一次。如果两个线程“同时”调用MyMethod,则它们会比赛。其中一个输掉比赛,并阻塞,直到MyClass构造函数在获胜线程上完成。

真的是这样的。

如果赢家线程上的构造函数调用了先前被失败者线程占用的锁定代码怎么办?那么你就有了一个典型的锁定顺序反转条件。您的程序陷入死循环。

如果你做某件事会导致问题,那就停止做那件事。永远不要在构造函数中执行可能阻塞的操作。

依靠构造函数初始化语义实施复杂的安全性要求是一个好主意吗?有一个构造函数可以进行用户交互是一个好主意吗?都不是一个好主意。我的建议是找到另一种方式来确保方法的安全前提条件得到满足。


5
Eric,我很好奇为什么你在这个回答中用“class constructor”或“cctor”代替了“static constructor”。使用“static constructor”来指代“cctor”是不合适的吗?请给予解释。 - phoog
6
我想保持术语的一致性,因此选择了最短的一个。“Static constructor”和“class constructor”都可以。作为实现细节,类型的静态构造函数被发射为一个名为“.cctor”的特殊方法,因此通常将这样的构造函数称为“cctor”。如果我在更正式的环境中写作,我会使用较长的术语。 - Eric Lippert
@EricLippert 这对于具有静态构造函数的非静态类也适用吗? - Legends
2
@Legends:非静态类中是否也适用于静态构造函数? 是的。 - Eric Lippert

24
根据MSDN的解释,静态构造函数是:

在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数以初始化类。

因此,在调用静态方法MyClass.MyMethod()之前将调用静态构造函数(前提是没有在静态构造函数或静态字段初始化期间被调用)。
如果您在静态构造函数中执行任何异步操作,则需要同步处理。

8
如果你在静态构造函数中进行任何涉及第二个线程的异步操作,你将会面临巨大的麻烦。这是最容易导致死锁的情况之一。请参考https://dev59.com/6Gox5IYBdhLWcg3w74q3#8883117 中的示例。 - Eric Lippert
@Eric:同意...我不想这样做,但是从他的示例中不确定他希望在调用MyMethod时完成什么... - James Michael Hare

11

#3 其实是 #1:静态初始化直到第一次使用属于它的类才会开始。

如果从静态构造函数或静态初始化块中调用了 MyMethod,那么这是可能的。如果您没有直接或间接地从静态构造函数中调用 MyMethod,那么应该没问题。


只是提醒一下,据我所知,static初始化实际上可以在第一次使用之前被调用,这取决于是否有资格进行优化。 - James Michael Hare
根据微软文档,@JamesMichaelHare所述:“静态构造函数的执行是由应用程序域中发生的以下事件中的第一个触发的:(1)创建类的实例。 (2)引用类的任何静态成员。”(http://msdn.microsoft.com/en-us/library/aa645612%28v=vs.71%29.aspx) - Sergey Kalinichenko
1
对于静态构造函数来说,没错,但对于静态初始化是我的重点。抱歉,也许我只是在挑剔“静态初始化不会开始……”这个短语,这对于静态构造函数是正确的,但如果类没有静态构造函数,则静态初始化可能会在之前发生。 - James Michael Hare
抱歉,我可能只是过度分析措辞。在问题的背景下,它绝对是正确的,我只是担心这个句子作为没有显式静态构造函数的类的静态初始化的独立语句。 - James Michael Hare
@James:你并没有过分分析——术语是关键的区别。静态构造函数是一个C#概念,而类型初始化则是.NET的东西。静态构造函数(C#)中的代码成为类型初始化器(.NET)的一部分,但何时以及如何触发类型初始化器(即“beforefieldinit”语义)取决于C#类是否具有静态构造函数。 - LukeH
@JamesMichaelHare:请查看Jon的页面,详细讨论了允许进行何种优化以及何时进行优化:http://csharpindepth.com/Articles/General/Beforefieldinit.aspx - Eric Lippert

9
文档中(重点强调我的):
静态构造函数会在创建第一个实例或引用任何静态成员之前自动调用以初始化类。

2

CLR保证静态构造函数在访问任何静态成员之前运行。然而,你的设计有点不太好。做类似这样的事情会更直接:

static void Main(string[] args) 
{ 
     bool userIsAuthenticated = MyClass.AuthenticateUser();
     if (userIsAuthenticated)
         MyClass.MyMethod(); 
 } 

如果认证失败,根据您的设计,阻止MyMethod运行的唯一方法是抛出异常。


2

在任何静态类方法被执行之前,保证已经调用了静态类的构造函数。例如:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Press enter");
        Console.ReadLine();
        Boop.SayHi();
        Boop.SayHi();
        Console.ReadLine();
    }

}

static class Boop
{
    static Boop()
    {
        Console.WriteLine("Hi incoming ...");
    }

    public static void SayHi()
    {
        Console.WriteLine("Hi there!");
    }
}

输出:

按回车键

// 按下回车键后

你好,来了...

你好!

你好!


使用 System 命名空间; namespace MyNameSpace { class Program { static void Main(string[] args) { Console.WriteLine("进入主函数"); Boop.SayHi(); Boop.SayHi(); } } static class Boop { static Boop() { Console.Read(); Console.WriteLine("构造函数已输入"); } public static void SayHi() { Console.WriteLine("方法被调用"); } } } 这个程序可以更好地理解。 - om471987
可能的。不过下次请将其发布为答案,这样更有帮助和可见性。 - haiyyu

2

如果您不从静态方法中创建类的实例,您可以保证4始终在2之后(但对于1和3不是如此)。


2
静态构造函数会在 mymethod 执行之前被调用。但是,如果 4 在 2 之前被调用,那么你就会出现问题,我建议重新考虑设计。总之,在静态构造函数中不应该做复杂的操作。

1
这是事情发生的实际顺序:
  1. 开始Main
  2. 开始静态MyClass构造函数
  3. 结束静态MyClass构造函数
  4. 开始MyMethod
  5. 结束Main

0

或者你可以通过调试器逐步执行。


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