为什么应该尽可能避免在静态构造函数中初始化静态字段?
C#具有一个默认开启的代码分析规则:[CA1810]Initialize reference type static fields inline,推荐我们以内联的方式初始化静态字段,而不是将初始化放在静态构造函数中。
一、两种初始化的性能差异
二、beforefieldinit标记
三、静态构造函数执行的时机
四、关于“All-Zero”结构体
五、RuntimeHelpers.RunClassConstructor方法
一、两种初始化的性能差异
CA1810这一规则与性能有关,我们可以利用如下这段简单的代码来演示两种初始化的性能差异。Foo和Bar这两个类的静态字段都定义了一个名为_value的静态字段,它们均通过调用静态方法Initialize返回的值进行初始化。不同的是Foo以内联(inline)赋值的方法进行初始化,而Bar则将初始化操作定义在静态构造函数中。假设Initialize方法是一个相对耗时的操作,我们利用Program的_initialized字段判断该方法是否被调用。
static class Program { private static bool _initialized; static void Main() { Foo.Invoke(); Debug.Assert(_initialized == false); Bar.Invoke(); Debug.Assert(_initialized == true); } private static int Initialize() { _initialized = true; return 123; } public class Foo { private readonly static int _value = Initialize(); public static int Value => _value; public static void Invoke() { } } public class Bar { private readonly static int _value; public static int Value => _value; static Bar() => _value = Initialize(); public static void Invoke() { } } }
从我们给出的调用断言可以确定,当我们调用Foo的静态方法Invoke时,它的静态字段_value并没有初始化;但是当我们调用Bar的Invoke方法时,Initialize方法会率先被调用来初始化静态字段。从这个例子来说,由于整个应用并没有使用到Foo和Bar的静态字段,所以针对它们的初始化是没有必要的。所以我们说以内联方式对静态字段进行初始化的Foo具有更好的性能。
二、beforefieldinit标记
对于Foo和Bar这两个类型表现出来的不同行为,我们可以试着从IL代码层面寻找答案。如下所示的两段IL代码分别来源于Foo和Bar,我们可以看到虽然Foo类中没有显式定义静态构造函数,但是编译器会创建一个默认的静态构造函数,针对静态字段的初始化就放在这里。我们可以进一步看出,自动生成的这个静态构造函数和我们自己写的并没有本质的不同。两个类型之间的差异并没有体现在静态构造函数上,而是在于:没有显式定义静态构造函数的Foo类型上具有一个beforefieldinit标记。
.class public auto ansi beforefieldinit Foo extends [System.Runtime]System.Object { .field private static initonly int32 _value .method private hidebysig specialname rtspecialname static void .cctor () cil managed { .maxstack 8 IL_0000: call int32 Program::Initialize() IL_0005: stsfld int32 Foo::_value IL_000a: ret }
… }
.class public auto ansi Bar extends [System.Runtime]System.Object { .field private static initonly int32 _value .method private hidebysig specialname rtspecialname static void .cctor () cil managed { .maxstack 8 IL_0000: call int32 Program::Initialize() IL_0005: stsfld int32 Bar::_value IL_000a: ret } }
三、静态构造函数执行的时机
从Foo和Bar的IL代码可以看出,针对它们静态字段的初始化都放在静态构造函数中。但是当我们调用一个并不涉及类型静态字段的Invoke方法时,定义在Foo中的静态构造函数会自动执行,但是定义在Bar中的则不会,由此可以看出一个类型的静态构造函数的执行时机与类型是否具有beforefieldinit标记有关。具体规则如下,这一个规则直接定义在CLI标准ECMA-335中,静态构造函数在此标准中被称为类型初始化器(Type Initializer)或者.cctor。
- 具有beforefieldinit标记:静态构造函数会在第一次读取任何一个静态字段之前自动执行,这相当于一种Lazy loading的模式;
- 不具有beforefieldinit标记:静态构造函数会在如下场景下自动执行:
- 第一次读取任何一个静态字段之前;
- 第一个执行任何一个静态方法之前;
- 引用类型:第一次调用构造函数之前;
- 值类型:第一次调用实例方法;
由于beforefieldinit标记只有在没有显式定义静态构造函数的情况下才会被添加,所以我们自行定义的专门用来初始化静态字段的静态构造函数是完全没有必要的。不但没有必要,还可能带来性能问题,应该改成以内联的形式对静态字段进行初始化。
四、关于“All-Zero”结构体
如果我们在一个结构体中显式定义了一个静态构造函数,当我们调用其构造函数之前,静态构造函数会自动执行。
public class Program { private static bool _initialized= false; static void Main() { var foobar = new Foobar(1, 2); Debug.Assert(_initialized == true); } public struct Foobar { static Foobar() => _initialized = true; public Foobar(int foo, int bar) { Foo = foo; Bar = bar; } public int Foo { get; } public int Bar { get; } } }
倘若按照如下的方式利用default关键字得到一个所有字段为“零”的默认结构体(all-zero structure),我们显式定义的静态构造函数是不会执行的。
public class Program { private static bool _initialized = false; static void Main() { Foobar foobar = default; Debug.Assert(foobar.Foo == 0); Debug.Assert(foobar.Bar == 0); Debug.Assert(_initialized == false); } ... }
五、RuntimeHelpers.RunClassConstructor方法
如果我们要确保某个类型的静态构造函数已经被显式调用,可以执行RuntimeHelpers.RunClassConstructor方法,它的参数为目标类型的TypeHandle。
public class Program { private static bool _initialized = false; static void Main() { RuntimeHelpers.RunClassConstructor(typeof(Foobar).TypeHandle); Debug.Assert(_initialized == true); }
… }
由于类型的静态构造函数只会被执行一次,所以多次RuntimeHelpers.RunClassConstructor并不会导致静态函数的重复执行。
public class Program { private static bool _initialized = false; static void Main() { RuntimeHelpers.RunClassConstructor(typeof(Foobar).TypeHandle); Debug.Assert(_initialized == true); _typeInitializerInvoked = false;
RuntimeHelpers.RunClassConstructor(typeof(Foobar).TypeHandle); Debug.Assert(_initialized == false); } ... }