【问题标题】:Potential pitfalls with static constructors in C#C# 中静态构造函数的潜在缺陷
【发布时间】:2012-05-15 00:13:29
【问题描述】:

我的问题是在重构一个只包含要声明为static 类的静态方法的类之后,并且在启动应用程序时遇到了奇怪的问题。

我没有进行任何彻底的调查,但似乎从静态构造函数中进行的某些调用由于某种原因没有完成。

那么,我想知道在 C# 中使用静态构造函数时有哪些陷阱?更具体地说,是否有任何事情应该不惜一切代价避免并且不能在静态构造函数中使用?

【问题讨论】:

    标签: c# .net static clr


    【解决方案1】:

    静态构造函数有几个陷阱。例如,如果是静态构造函数 throws an exception,则无论何时访问其任何成员,您都将继续获得 TypeInitializationException

    如果静态构造函数抛出异常,运行时将不会再次调用它,并且该类型将在程序运行的应用程序域的生命周期内保持未初始化状态。

    一般来说,静态类应该只用在不需要任何初始化的无状态场景中。如果你的类需要初始化,你最好使用singleton pattern,第一次访问时可以是lazily initialized

    public class MyClass
    {
        private static readonly Lazy<MyClass> current = 
            new Lazy<MyClass>(() => new MyClass());
    
        public static MyClass Current
        {
            get { return current.Value; }
        }
    
        private MyClass()
        {
            // Initialization goes here.
        }
    
        public void Foo()
        {
            // ...
        }
    
        public void Bar()
        {
            // ...
        }
    }
    
    static void Main(string[] args)
    {
        MyClass.Current.Foo();   // Initialization only performed here.
        MyClass.Current.Bar();
        MyClass.Current.Foo();
    }
    

    编辑:我对此事做了一些进一步的阅读,如果您执行阻塞操作(例如异步回调或线程同步),静态构造函数确实会导致死锁) 在其中。

    CLR 在内部使用锁定来防止类型初始值设定项(静态构造函数)同时执行多次。因此,如果您的静态构造函数试图从另一个线程访问其声明类型的另一个成员,它将不可避免地死锁。由于“另一个成员”可能是声明为 PLINQ 或 TPL 操作的一部分的匿名函数,因此这些错误可能很微妙且难以识别。

    Igor Ostrovsky (MSFT) 在他的 Static constructor deadlocks 文章中对此进行了解释,并提供了以下死锁示例:

    using System.Threading;
    
    class MyClass
    {
        static void Main() { /* Won’t run... the static constructor deadlocks */  }
    
        static MyClass()
        {
            Thread thread = new Thread(arg => { });
            thread.Start();
            thread.Join();
        }
    }
    

    在上面的例子中,新线程需要访问空的匿名函数{ },定义为它的回调。但是,由于匿名函数在后台被编译为MyClass 的另一个私有方法,所以在MyClass 类型初始化之前,新线程无法访问它。而且,由于MyClass 静态构造函数需要先等待新线程完成(因为thread.Join()),因此会出现死锁。

    【讨论】:

    • 除了构造函数内部抛出的异常之外还有什么?例如,什么可以解释我遇到的类似“僵局”的情况?是否在幕后以某种方式涉及静态类型的任何锁定?
    • 第一个示例中的内容与这些行之间有什么区别吗? private static readonly Lazy&lt;MyClass&gt; current; static MyClass { current = new Lazy&lt;MyClass&gt;(() =&gt; new MyClass()); }(抱歉,格式似乎不正确:))
    • @Mark:我相信这两者是等价的。您的静态构造函数唯一要做的就是将 Lazy 分配给静态字段(没有对其进行初始化),因此它没有失败的机会(即使 MyClass() 构造函数抛出异常)。在这两种情况下,单例仅在第一次调用 MyClass.Current 时才被初始化(延迟)。
    • 感谢您提供完整的示例!对于那些想知道上述单例是否是线程安全的和“最佳”解决方案(这是隐含的,但我想要明确的)的人,答案是肯定的。请参阅Jon Skeet's article on singletons in C#this similar S.O. question
    • 我很高兴看到您的编辑。我正试图做这件事,但不知道为什么它不起作用。非常感谢!
    【解决方案2】:

    这不是问题的答案,但是评论太长了,所以我在这里提供...

    由于我不知道 static class 构造,我使用以下方案(简化)为我提供单例:

    public class SomeSingleton {
        static _instance;
        static public SomeSingleton Instance {
            get {
                if (_instance==null) {
                    _instance=new SomeSingleton();
                }
                return _instance;
            }
        }
    }
    

    以后,你用

    SomeSingleton.Instance.MyProp = 3;
    

    第一次使用 Instance 成员将构建您的单例。

    我想这是可以的,因为如果有很多这样的类,单例的实例化是按正确的顺序完成的。

    【讨论】:

    • 它没有回答问题...而且静态类与单例相同(例如,您不能将静态类作为参数传递,你可以用单例做)
    • 您的初始化不是线程安全的。如果多个线程同时访问Instance 属性,它们可能会获得“单例”的不同实例。在特定情况下,这可能是也可能不是问题,但它总体上打破了单例范式。如果您使用的是 .NET 4(或更高版本),则应切换到 Lazy&lt;T&gt;;如果没有,你应该考虑使用lock来同步初始化。
    【解决方案3】:

    是的,有一些陷阱,主要与类的初始化时间有关。基本上,具有静态构造函数的类不会被标记为beforefieldinit 标志,这允许运行时在稍后对其进行初始化。

    查看this article了解更多详情。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-05-06
      • 1970-01-01
      • 2011-10-07
      • 2011-04-19
      • 2011-07-16
      • 1970-01-01
      • 2013-01-16
      • 1970-01-01
      相关资源
      最近更新 更多