為什麼應該儘可能避免在靜態建構函式中初始化靜態欄位?

2023-07-10 09:00:22

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 Utility::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 Utility::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 ); }

… }

由於型別的靜態建構函式只會被執行一次,所以多次RuntimeHelpers.RunClassConstructor並不會導致靜態函數的重複執行。

public class Program
{
    private static bool _initialized = false;
    static void Main()
    {
        RuntimeHelpers.RunClassConstructor(typeof(Foobar).TypeHandle);
        Debug.Assert(_initialized == true);
        _typeInitializerInvoked = false;
        Debug.Assert(_initialized == false);
    }
   ...
}