C#中静态构造函数的潜在陷阱

19

在将只包含静态方法的类重构为声明为 static 类后,我遇到了一些奇怪的问题,这导致应用程序启动时出现了问题。

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

因此,我想知道在使用 C# 中的静态构造函数时是否存在任何陷阱?更具体地说,是否有任何必须绝对避免并且不得从静态构造函数中使用的东西?

3个回答

27

静态构造函数存在几个陷阱。例如,如果一个静态构造函数抛出异常,每当您访问其任何成员时,都会继续收到TypeInitializationException

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

通常情况下,静态类只应在无状态场景中使用,其中您不需要任何初始化。如果您的类需要初始化,您可能最好使用单例模式,可以在第一次访问时延迟初始化

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()),导致死锁发生。

除了构造函数中抛出的异常之外,还有其他什么原因吗?例如,什么可能解释我正在经历的“死锁”类似情况?是否在静态类型背后某种程度上涉及任何锁定? - lysergic-acid
第一个示例和这些行之间有什么区别吗?
private static readonly Lazy<MyClass> current; static MyClass { current = new Lazy<MyClass>(() => new MyClass()); }
- Mark
@Mark:我认为这两者是等价的。你的静态构造函数所做的唯一事情就是将Lazy分配给静态字段(而不是初始化它),因此它没有失败的机会(即使MyClass()构造函数抛出异常)。在这两种情况下,单例只有在第一次调用MyClass.Current时才会被初始化(懒加载)。 - Douglas
感谢提供完整的示例!对于那些想知道上面的单例模式是否线程安全和是否是“最佳”解决方案的人(虽然这是暗示,但我想要明确的答案),答案是肯定的。请参阅Jon Skeet关于C#中单例模式的文章类似的S.O.问题 - goku_da_master
我非常高兴看到你的编辑。我一直在尝试做这件事,但不知道为什么它不起作用。非常感谢! - Matt Davis

3

是的,有一些陷阱,大多与类何时初始化有关。基本上,具有静态构造函数的类将不被标记为 beforefieldinit 标志,这使得运行时可以在稍后的时间初始化它。

更多细节请参见此文章


0

这不是对问题的回答,但它太长了不能作为评论,所以我在这里提供它...

由于我不知道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 成员将构造您的单例模式。

我猜这是可以的,因为如果有许多这样的类的单例模式实例化是按适当的顺序完成的。


1
它没有回答这个问题...而静态类与单例不同(例如,您无法将静态类作为参数传递,而可以使用单例)。 - Thomas Levesque
5
你的初始化不具备线程安全性。如果Instance属性被多个线程同时访问,它们可能会得到“单例”的不同实例。在特定情况下,这可能或者不是一个问题,但它总体上破坏了单例模式。如果你使用的是.NET 4(或者更高版本),应该切换到Lazy<T>;如果不是,则应该考虑使用lock来同步初始化。 - Douglas

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