如何确保调用基类的静态构造函数?

39

C# 中的静态构造函数文档中提到:

静态构造函数用于初始化任何静态数据,或执行仅需一次完成的特定操作。在创建第一个实例或引用任何静态成员之前,它会被自动调用。

这个最后一句话(关于何时自动调用)让我有些困惑;在读到这句话之前,我认为只要以任何方式访问一个类,我就可以确定基类的静态构造函数已经被调用了。但是测试和文档的检查表明这并不是情况。似乎直到访问该基类的成员之前,基类的静态构造函数才可能被调用。

现在,如果我只处理派生类的静态成员,该怎么办呢?

为了更加具体,我原本以为下面的代码会正常工作:

abstract class TypeBase
{
    static TypeBase()
    {
        Type<int>.Name = "int";
        Type<long>.Name = "long";
        Type<double>.Name = "double";
    }
}

class Type<T> : TypeBase
{
    public static string Name { get; internal set; }
}

class Program
{
    Console.WriteLine(Type<int>.Name);
}

我原以为访问 Type<T> 类会自动调用 TypeBase 的静态构造函数,但事实并非如此。 Type<int>.Name 为空,并且上述代码输出空字符串。

除了创建一些虚拟成员(比如一个什么都不做的静态 Initialize() 方法),是否有更好的方法确保基类的静态构造函数在使用任何派生类型之前被调用?

如果没有,那就只能用虚拟成员!


除非这只是为了问题而提供的示例代码,否则你不可以直接使用public static string Name { get { return typeof(T).Name; } }吗? - Matt Greer
@Matt:抱歉,我应该让这一点更清楚,你是对的:这只是为了问题而提供的示例代码。 - Dan Tao
将抽象类用作具体示例?;-) - Geeb
6个回答

28

您可以显式地调用静态构造函数,这样您将不必为初始化创建任何方法:

System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof (TypeBase).TypeHandle);

您可以在派生类的静态构造函数中调用它。


4
这似乎有效。与任何静态初始化顺序解决方案一样,我对其是否万无一失持怀疑态度,但至少在简单的单线程应用程序中,如果您两次调用此函数,它不会运行静态构造函数两次。 - Merlyn Morgan-Graham
这个方法的性能有什么想法?它是否使用了反射,而且如果静态构造函数已经被调用,它是否很快? - devios1
这是实现的方法 - 在静态构造函数中复制base(),只需在每个构造函数的开头调用RuntimeHelpers.RunClassConstructor(typeof(TypeBase).TypeHandle)。 - Tod Thomson

20

如其他人所指出的,你的分析是正确的。这里非常遵循规范实现;因为没有调用基类的任何成员和创建实例,所以不会调用基类的静态构造函数。我可以看出这可能会让人感到惊讶,但这是规范的严格和正确实现。

除了“如果做某事会痛,就不要那样做”之外,我没有其他建议。我只是想指出相反的情况也可能会咬你一口:

class Program 
{
  static void Main(string[] args)
  {      
    D.M();
  }      

}
class B 
{ 
  static B() { Console.WriteLine("B"); }
  public static void M() {}
} 
class D: B 
{ 
  static D() { Console.WriteLine("D"); }
}

尽管已经调用了 "属于 D 的成员",但是这会打印出 "B"。 M 仅通过继承成为 D 的成员;CLR 无法区分是 "通过 D" 还是 "通过 B" 调用了 B.M。


14

这里的规则非常复杂,在CLR 2.0和CLR 4.0之间,它们实际上以微妙而有趣的方式发生了变化,我认为这使得大多数“聪明”的方法在CLR版本之间变得脆弱。如果Initialize()方法不触及字段,在CLR 4.0中也可能无法完成工作。

我会寻找替代设计,或者在您的类型中使用常规延迟初始化(即检查位或引用(针对null)是否已完成)。


我成功地调用了一个虚拟的Initialize()成员函数,它没有与CLR 4.0中的字段进行交互。这是根据我在答案中发布的代码。 - Joshua Rodgers
只要基类有静态构造函数(不仅是字段初始化器),那么调用“Initialize”就一定会调用该构造函数,因为该类型不会被标记为“beforefieldinit”。正如规范所说,静态构造函数将在“创建第一个实例或引用任何静态成员之前”被调用。既然问题是关于如何强制执行静态构造函数,那么我们可以假设它会存在。 - LukeH
话虽如此,依赖规范的晦涩角落来保证功能并不是一个好主意,特别是当一些不熟悉规范的人在几年后来更新代码时。最好还是像你建议的那样做一些明确和显然正确的事情。 - LukeH
我曾经试图实现我在这个答案中提出的想法,该想法基于SLaks的建议使用通用静态类型而不是字典。我的想法是从非泛型类型派生并使用该基类型的静态构造函数来初始化“字典”,这将是一种优雅的解决方案;现在我意识到它需要一个hack来利用这样的构造函数(例如,一个虚拟成员),我认为你是正确的(续)。 - Dan Tao
...而且完全不同的设计会更有意义。我想做的是完全放弃“基本类型”的想法,只是使用一个单独的internal类来执行我想要的初始化。 - Dan Tao
1
第二个链接已经失效 :( - Erik Philips

3

在我的所有测试中,我只能通过调用基类的虚成员函数来触发基类调用其静态构造函数,示例如下:

class Base
{
    static Base()
    {
        Console.WriteLine("Base static constructor called.");
    }

    internal static void Initialize() { }
}

class Derived : Base
{
    static Derived()
    {
        Initialize(); //Removing this will cause the Base static constructor not to be executed.
        Console.WriteLine("Derived static constructor called.");
    }

    public static void DoStaticStuff()
    {
        Console.WriteLine("Doing static stuff.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Derived.DoStaticStuff();
    }
}

另一种选择是在派生类型中包含一个静态只读成员,执行以下操作: private static readonly Base myBase = new Base(); 然而,这样做感觉有点像 hack(虽然虚拟成员也是),只是为了调用基类的静态构造函数。

是的,确实如此... 但也许这是唯一的方法? - Dan Tao
是的,看起来这似乎是唯一的方法。除了你提供的解决方案和创建基础的虚拟静态字段之外,我找不到其他的做法了。 - Joshua Rodgers

3

我几乎总是后悔依赖这样的东西。静态方法和类可能会在以后限制你。如果您想稍后为Type类编写一些特殊行为,那么您将被限制。

因此,这里是对您方法的轻微变化。它需要更多的代码,但它将允许您稍后定义自定义Type,以便您可以执行自定义操作。

    abstract class TypeBase
    {
        private static bool _initialized;

        protected static void Initialize()
        {
            if (!_initialized)
            {
                Type<int>.Instance = new Type<int> {Name = "int"};
                Type<long>.Instance = new Type<long> {Name = "long"};
                Type<double>.Instance = new Type<double> {Name = "double"};
                _initialized = true;
            }
        }
    }

    class Type<T> : TypeBase
    {
        private static Type<T> _instance;

        public static Type<T> Instance
        {
            get
            {
                Initialize();
                return _instance;
            }
            internal set { _instance = value; }
        }

        public string Name { get; internal set; }
    }

当你想要为Type添加一个虚拟方法并且需要一个特殊的实现时,你可以这样实现:

class TypeInt : Type<int>
{
    public override string Foo()
    {
        return "Int Fooooo";
    }
}

然后通过更改 < /p> 连接它。
protected static void Initialize()
{
      if (!_initialized)
      {
          Type<int>.Instance = new TypeInt {Name = "int"};
          Type<long>.Instance = new Type<long> {Name = "long"};
          Type<double>.Instance = new Type<double> {Name = "double"};
          _initialized = true;
       }
}

我的建议是避免使用静态构造函数 - 这很容易做到。同时尽可能避免使用静态类和静态成员。我并不是说永远不要使用,只是要谨慎使用。更倾向于使用一个类的单例而不是静态。


+1:虽然这并没有解决具体的问题,但我认为这是很好的建议。 - Merlyn Morgan-Graham
3
...并且喜欢使用IoC容器而不是单例模式。 :) - TrueWill
这是一个好主意,但实现不是线程安全的(对于像这样的东西,它确实应该是线程安全的)。 - T McKeown

0

只是一个想法,你可以这样做:

    abstract class TypeBase
    {
        static TypeBase()
        {
            Type<int>.Name = "int";
            Type<long>.Name = "long";
            Type<double>.Name = "double";
        }
    }

    class Type<T> : TypeBase
    {
        static Type() 
        {
            new Type<object>();
        }

        public static string Name { get; internal set; }
    }

    class Program
    {
        Console.WriteLine(Type<int>.Name);
    }

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